Merge branch 'dev'

This commit is contained in:
N-Pex 2023-05-17 09:06:25 +02:00
commit f570d2ba90
66 changed files with 2498 additions and 1580 deletions

View file

@ -38,13 +38,26 @@ npm run build
npm run lint npm run lint
``` ```
### Customize configuration ### Customize build configuration
See [Configuration Reference](https://cli.vuejs.org/config/). See [Configuration Reference](https://cli.vuejs.org/config/).
## Theming ## 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>" * 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) * Insert the resulting config blob into the "shortCodeStickers" value of the config file (assets/config.json)
* Rearrange order of sticker packs by editing the config blob above. * Rearrange order of sticker packs by editing the config blob above.
### Attributions
Sounds from [Notification Sounds](https://notificationsounds.com)

741
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,7 @@
"linkify-html": "^4.1.0", "linkify-html": "^4.1.0",
"linkifyjs": "^4.1.0", "linkifyjs": "^4.1.0",
"material-design-icons-iconfont": "^6.1", "material-design-icons-iconfont": "^6.1",
"matrix-js-sdk": "^19.7.0", "matrix-js-sdk": "^23.4.0",
"md-gum-polyfill": "^1.0.0", "md-gum-polyfill": "^1.0.0",
"mic-recorder-to-mp3": "^2.2.2", "mic-recorder-to-mp3": "^2.2.2",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",

View file

@ -63,7 +63,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.log("Error creating client", 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 // 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... // of server cleanup of accounts or similar. Wipe account and restart...
this.$store.commit("setUser", null); this.$store.commit("setUser", null);
@ -145,10 +145,10 @@ export default {
}, },
favicon() { favicon() {
var favicon = 'favicon.ico'; var favicon = this.$config.logo ? this.$config.logo : 'favicon.ico';
if (this.$route.meta.includeFavicon) { if (this.$route.meta.includeFavicon) {
if (this.$matrix.currentRoom) { if (this.$matrix.currentRoom) {
favicon = this.$matrix.currentRoom.avatar || 'favicon.ico'; favicon = this.$matrix.currentRoom.avatar || favicon;
} }
} }
return favicon; return favicon;

View file

@ -8,7 +8,10 @@
"languageSupportEmail": "support@guardianproject.info", "languageSupportEmail": "support@guardianproject.info",
"productLink": "letsconvene.im", "productLink": "letsconvene.im",
"defaultServer": "https://neo.keanu.im", "defaultServer": "https://neo.keanu.im",
"identityServer_unset": "",
"rtl": false, "rtl": false,
"accentColor_unset": "",
"logo_unset": "",
"analytics": [ "analytics": [
{ {
"enabled": true, "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"
} }

View file

@ -0,0 +1 @@
// Sass Mixins

View file

@ -8,10 +8,12 @@ $admin-fg: white;
body { body {
--v-background-color: white; --v-background-color: white;
--v-foreground-color: black; --v-foreground-color: black;
--v-secondary-color: #242424;
--v-divider-color: #eeeeee; --v-divider-color: #eeeeee;
&.dark { &.dark {
--v-background-color: black; --v-background-color: black;
--v-foreground-color: white; --v-foreground-color: white;
--v-secondary-color: #c0c0c0;
--v-divider-color: rgba(221, 221, 221, 0.1); --v-divider-color: rgba(221, 221, 221, 0.1);
} }
} }
@ -45,15 +47,32 @@ body {
border-bottom: 1px solid var(--v-divider-color); border-bottom: 1px solid var(--v-divider-color);
.chat-header-row { .chat-header-row {
margin: 0; margin: 0;
padding: 4px 10px; padding: 4px 10px 4px 28px;
align-items: center; align-items: center;
height: 100%; 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 { .chat-header-name {
overflow: hidden; width: 0px; // Set to 0 for flexbox autosize
cursor: pointer; cursor: pointer;
} }
.chat-header-members {
overflow: hidden;
}
.num-members { .num-members {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
font-weight: 400; font-weight: 400;
@ -73,9 +92,8 @@ body {
margin-bottom: $chat-standard-padding-xs; margin-bottom: $chat-standard-padding-xs;
} }
@media #{map-get($display-breakpoints, 'sm-and-down')} { .close-button .v-icon {
position: fixed; color: var(--v-secondary-color);
z-index: 10;
} }
.icon-dropdown { .icon-dropdown {
@ -84,33 +102,66 @@ body {
.notification-alert { .notification-alert {
display: inline-block; display: inline-block;
background-color: #ff3300; background-color: $alert-bg-color;
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 4px; border-radius: 4px;
margin-bottom: 2px; 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 { .chat-root {
@ -128,11 +179,6 @@ body {
background-color: $chat-background; background-color: $chat-background;
overflow: hidden; overflow: hidden;
.chat-room-invitations {
padding: 10px;
background-color: #f2f2f2;
}
.chat-content { .chat-content {
margin: 0; margin: 0;
padding-top: $chat-standard-padding-s; padding-top: $chat-standard-padding-s;
@ -224,7 +270,7 @@ body {
padding: 0; padding: 0;
min-width: 48px; min-width: 48px;
&.input-more-icon { &.input-more-icon {
svg { svg {
fill: black; fill: black;
} }
@ -250,9 +296,23 @@ body {
/* Remove text underline */ /* Remove text underline */
color: transparent !important; color: transparent !important;
min-height: 20px; 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')} { @media #{map-get($display-breakpoints, 'sm-and-down')} {
position: fixed; position: fixed;
bottom: 0px; bottom: 0px;
@ -292,6 +352,44 @@ body {
.message-wrapper { .message-wrapper {
position: relative; position: relative;
user-select: none; 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 { .messageIn {
@ -378,6 +476,13 @@ body {
.link { .link {
color: inherit; color: inherit;
} }
.quick-reaction-container {
margin-left: 42px;
[dir="rtl"] & {
margin-right: 42px;
}
}
} }
.messageOut { .messageOut {
@ -479,6 +584,13 @@ body {
.link { .link {
color: inherit; color: inherit;
} }
.quick-reaction-container {
margin-right: 42px;
[dir="rtl"] & {
margin-left: 42px;
}
}
} }
.sender, .sender,
@ -652,44 +764,6 @@ body {
text-align: center; 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 { .download-overlay {
position: absolute; position: absolute;
left: 0; left: 0;
@ -709,7 +783,7 @@ body {
.room-name-inline { .room-name-inline {
font-family: "Poppins", sans-serif; font-family: "Poppins", sans-serif;
font-weight: 700; font-weight: 700;
font-size: 18 * $chat-text-size; font-size: 16 * $chat-text-size;
text-transform: uppercase; text-transform: uppercase;
color: var(--v-foreground-color); color: var(--v-foreground-color);
text-align: center; 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 { .room-info {
background-color: #e8e8e8; background-color: #e8e8e8;
height: 100%; height: 100%;
@ -802,7 +920,6 @@ body {
height: 64px !important; height: 64px !important;
margin-bottom: 20px; margin-bottom: 20px;
cursor: default; cursor: default;
.headline { .headline {
font-size: 70 * $chat-text-size !important; font-size: 70 * $chat-text-size !important;
} }
@ -1159,6 +1276,26 @@ body {
} }
text-transform: none !important; 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 { .room-link .v-input__slot::before {
@ -1275,11 +1412,11 @@ body {
background-color: var(--v-background-color); background-color: var(--v-background-color);
color: var(--v-foreground-color); color: var(--v-foreground-color);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
.load-earlier { .load-earlier {
flex: 1 0 auto; flex: 1 0 auto;
padding: 20px; padding: 20px;
@ -1297,7 +1434,8 @@ body {
height: 32px !important; height: 32px !important;
margin-left: -8px !important; margin-left: -8px !important;
} }
.list-enter-active, .list-leave-active { .list-enter-active,
.list-leave-active {
transition: all 1s; transition: all 1s;
} }
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ { .list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
@ -1310,7 +1448,7 @@ body {
padding: 20px; padding: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items:center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
} }
@ -1372,12 +1510,28 @@ body {
height: 103px !important; height: 103px !important;
margin: 0 !important; margin: 0 !important;
} }
#btn-play, #btn-pause { #btn-play,
#btn-pause {
margin: 26px; margin: 26px;
} }
.mic-button { .mic-button {
z-index: 0; 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 { .audio-layout.voice-recorder {
@ -1385,4 +1539,4 @@ body {
right: 20px; right: 20px;
bottom: 20px; bottom: 20px;
position: absolute; position: absolute;
} }

View file

@ -1,5 +1,6 @@
@import './variables'; @import './variables';
@import './utilities'; @import './utilities';
@import './mixins';
@font-face { @font-face {
font-family: "Inter"; font-family: "Inter";
@ -122,6 +123,12 @@ body { position:absolute; top:0; bottom:0; right:0; left:0; }
min-height: $chat-standard-padding !important; min-height: $chat-standard-padding !important;
margin-top: $chat-standard-padding-xs; margin-top: $chat-standard-padding-xs;
margin-bottom: $chat-standard-padding-xs; margin-bottom: $chat-standard-padding-xs;
.v-icon {
height: 14px;
width: 14px;
margin-right: 8px;
}
} }
.v-dialog { .v-dialog {

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -1,6 +1,6 @@
<template> <template>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <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="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="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="currentColor"/>
</svg> </svg>
</template> </template>

View 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>

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

View file

@ -3,11 +3,6 @@
"name": "འདུ་འཛོམས།", "name": "འདུ་འཛོམས།",
"tag_line": "འབྲེལ་མཐུད་བྱོས།" "tag_line": "འབྲེལ་མཐུད་བྱོས།"
}, },
"global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
},
"fallbacks": { "fallbacks": {
"download_name": "ཕབ་ལེན།", "download_name": "ཕབ་ལེན།",
"original_text": "<མ་ཡིག>", "original_text": "<མ་ཡིག>",
@ -35,7 +30,16 @@
"permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་ཁག", "permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་ཁག",
"created_by": "{user} བཟོས།", "created_by": "{user} བཟོས།",
"copy_link": "གདན་ཞུ་འབྲེལ་ཐག་པར་བཤུས་རྒྱོབས།", "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": { "invite": {
"done": "ལེགས་འགྲུབ།", "done": "ལེགས་འགྲུབ།",
@ -53,7 +57,15 @@
"username_required": "སྤྱོད་མིང་དགོས་ཀྱི་ཡོད།", "username_required": "སྤྱོད་མིང་དགོས་ཀྱི་ཡོད།",
"create_room": "ཐོ་འགོད་དང་ཁ་བརྡ་ཁང་གསར་སྐྲུན།", "create_room": "ཐོ་འགོད་དང་ཁ་བརྡ་ཁང་གསར་སྐྲུན།",
"or": "ཡང་ན།", "or": "ཡང་ན།",
"invalid_message": "རྒྱུན་སྤྱོད་མིང་ངམ་གསང་ཚིག་འགྲིག་མི་འདུག" "invalid_message": "རྒྱུན་སྤྱོད་མིང་ངམ་གསང་ཚིག་འགྲིག་མི་འདུག",
"terms": "ཁྱིམ་སྤྱོད་དྲ་བའི་ཞབས་ཞུ་ཆས་ཀྱིས་ཁྱེད་ལ་གཤམ་གྱི་སྲིད་ཇུས་འདི་དག་དང་ལེན་བྱེད་དགོས་པའི་རེ་སྐུལ་བྱེད་ཀྱིན་འདུག",
"accept_terms": "དང་ལེན།",
"email": "ཁྱེད་ཀྱིས་སོ་སོའི་ཡིག་ཟམ་ཁ་བྱང་ར་སྤྲོད་བྱེད་དགོས།",
"resend_verification": "ར་སྤྲོད་ཡིག་ཟམ་བསྐྱར་དུ་ཐོངས།",
"send_verification": "ར་སྤྲོད་ཡིག་ཟམ་ཐོངས།",
"no_supported_flow": "བཀོལ་ཆས་འདི་སྤྲད་ཡོད་པའི་དྲ་བའི་ཞབས་ཞུ་ཆས་ནང་དུ་འཛུལ་ཐུབ་ཀྱི་མི་འདུག",
"sent_verification": "{email}ཡིག་ཟམ་ཁ་བྱང་འདིའི་ཐོག་ཏུ་འཕྲིན་པ་ཞིག་བཏང་ཡོད། ཡིག་ཟམ་ནང་འཛུལ་ཏེ་སོ་སོའི་ཡིག་ཟམ་ཁ་བྱང་ར་སྤྲོད་བྱེད་རོགས།",
"email_not_valid": "ཡིག་ཟམ་ཁ་བྱང་བེད་མེད་རེད་འདུག"
}, },
"new_room": { "new_room": {
"next": "རྗེས་མ།", "next": "རྗེས་མ།",
@ -72,11 +84,12 @@
"set_join_permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་སྒྲིག་འགོད་བྱོས།", "set_join_permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་སྒྲིག་འགོད་བྱོས།",
"join_permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་ཁག", "join_permissions": "ནང་འཛུལ་གྱི་ཆོག་མཆན་ཁག",
"new_room": "ཁ་བརྡ་ཁང་གསར་པ།", "new_room": "ཁ་བརྡ་ཁང་གསར་པ།",
"name_room": "ཁ་བརྡ་ཁང་ལ་མིང་ཐོགས།", "name_room": "ཁ་བརྡ་ཁང་གི་མིང་།",
"room_topic": "གལ་ཏེ་འདོད་པ་ཡོད་ན། ཚོགས་པའི་སྐོར་གྱི་འགྲེལ་བཤད་ཐུང་ངུ་ཞིག་འབྲི་ཆོག", "room_topic": "གལ་ཏེ་འདོད་པ་ཡོད་ན། ཚོགས་པའི་སྐོར་གྱི་འགྲེལ་བཤད་ཐུང་ངུ་ཞིག་འབྲི་ཆོག",
"create": "བཟོས།", "create": "བཟོས།",
"room_name_limit_error_msg": "", "colon_not_allowed": ": རྟགས་འདི་བཀོལ་མི་ཆོག",
"colon_not_allowed": "" "options": "གདམ་ག",
"room_name_limit_error_msg": "ཡིག་འབྲུ་གྲངས་༥༠ ལས་བརྒལ་མི་ཆོག"
}, },
"menu": { "menu": {
"logout": "ཕྱིར་ཐོན།", "logout": "ཕྱིར་ཐོན།",
@ -95,7 +108,12 @@
"loading": "{appName} སྣོན་འཇུག་བྱེད་བཞིན་པ།", "loading": "{appName} སྣོན་འཇུག་བྱེད་བཞིན་པ།",
"ignore": "སྣང་མེད་ཐོངས།", "ignore": "སྣང་མེད་ཐོངས།",
"join": "ཞུགས།", "join": "ཞུགས།",
"undo": "ཕྱིར་བསྡུ།" "undo": "ཕྱིར་བསྡུ།",
"user_kick_and_ban": "སྤྱོད་མཁན་འདི་ཕྱིར་འབུད་དང་བཀག་སྡོམ་བྱོས།",
"user_make_admin": "འཛིན་སྐྱོང་པ་བཟོས།",
"user_make_moderator": "གཙོ་སྐྱོང་བ་བཟོས།",
"user_revoke_moderator": "གཙོ་སྐྱོང་བའི་དབང་ཚད་མེད་པར་བཟོས།",
"user_kick": "སྤྱོད་མཁན་འདི་སྒོར་ཕུད།"
}, },
"profile": { "profile": {
"change_password": "གསང་ཚིག་རྗེས།", "change_password": "གསང་ཚིག་རྗེས།",
@ -112,7 +130,7 @@
"language_description": "འདུ་འཛོམས་སྐད་ཡིག་མང་པོའི་ནང་དུ་ཡོད།", "language_description": "འདུ་འཛོམས་སྐད་ཡིག་མང་པོའི་ནང་དུ་ཡོད།",
"dont_see_yours": "ཁྱེད་ཀྱི་མིང་མཐོང་གི་མི་འདུག་གམ།", "dont_see_yours": "ཁྱེད་ཀྱི་མིང་མཐོང་གི་མི་འདུག་གམ།",
"tell_us": "ང་ཚོར་ཤོོད།", "tell_us": "ང་ཚོར་ཤོོད།",
"display_name_required": "" "display_name_required": "འཆར་མིང་དགོས།"
}, },
"device_list": { "device_list": {
"not_verified": "ར་སྤྲོད་བྱས་མི་འདུག", "not_verified": "ར་སྤྲོད་བྱས་མི་འདུག",
@ -137,11 +155,14 @@
"room_list_rooms": "ཁ་བརྡ་ཁང་།", "room_list_rooms": "ཁ་བརྡ་ཁང་།",
"room_list_invites": "གདན་ཞུ་ཁག", "room_list_invites": "གདན་ཞུ་ཁག",
"purge_failed": "ཁ་བརྡ་ཁང་བཤིག་ཐུབ་མ་སོང་།", "purge_failed": "ཁ་བརྡ་ཁང་བཤིག་ཐུབ་མ་སོང་།",
"purge_removing_members": "ཚོགས་མི་ཁག་ཕྱིར་འདོན།", "purge_removing_members": "ཚོགས་མི་ཁག་ཕྱིར་འདོན། {total})་ཀྱི་({members}",
"purge_redacting_events": "ཁ་བརྡ་གཙང་གསུབ།", "purge_redacting_events": "ཁ་བརྡ་གཙང་གསུབ། {total})་ཀྱི་({count}",
"purge_set_room_state": "ཁ་བརྡ་ཁང་གི་རྣམ་པ་སྒྲིག་འགོད།", "purge_set_room_state": "ཁ་བརྡ་ཁང་གི་རྣམ་པ་སྒྲིག་འགོད།",
"room_name_required": "", "room_list_new_messages": "{count} ཆ་འཕྲིན་གསར་པ།",
"room_topic_required": "" "room_topic_required": "ཁ་བརྡ་ཁང་ལ་འགྲེལ་བཤད་དགོས།",
"room_name_required": "ཁ་བརྡ་ཁང་ལ་མིང་ཞིག་དགོས།",
"invitations": "ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་གང་ཡང་མི་འདུག | ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་གྲངས་གཅིག་འདུག | ཁྱེད་ལ་གྲོགས་པོའི་གདན་ཞུ་{གྲངས}་འདུག",
"unseen_messages": "ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་གང་ཡང་མི་འདུག | ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་གཅིག་འདུག | ཁྱེད་ཀྱིས་མཐོང་མེད་པའི་ཆ་འཕྲིན་{count}འདུག"
}, },
"message": { "message": {
"users_are_typing": "{count} ཚོགས་མི་ཡིས་གཏགས་བཞིན་འདུག", "users_are_typing": "{count} ཚོགས་མི་ཡིས་གཏགས་བཞིན་འདུག",
@ -180,12 +201,21 @@
"room_powerlevel_change": "{user} {changes} ཡི་སྟོབས་ཤུགས་གནས་རིམ་བརྗེས་སོང་།", "room_powerlevel_change": "{user} {changes} ཡི་སྟོབས་ཤུགས་གནས་རིམ་བརྗེས་སོང་།",
"user_changed_guest_access_open": "{user} མགྲོན་པོ་ཁ་བརྡ་ཁང་དུ་འཛུལ་བཅུག་སོང་།", "user_changed_guest_access_open": "{user} མགྲོན་པོ་ཁ་བརྡ་ཁང་དུ་འཛུལ་བཅུག་སོང་།",
"user_changed_guest_access_closed": "{user} མགྲོན་པོ་ཁ་བརྡ་ཁང་དུ་འཛུལ་བཅུག་མ་སོང་།", "user_changed_guest_access_closed": "{user} མགྲོན་པོ་ཁ་བརྡ་ཁང་དུ་འཛུལ་བཅུག་མ་སོང་།",
"reply_image": "", "reply_image": "པར་རིས།",
"reply_audio_message": "", "reply_audio_message": "སྐད་བརྡའི་ཆ་འཕྲིན།",
"reply_video": "", "reply_video": "བརྙན།",
"reply_poll": "", "user_was_banned_you": "ཁྱེད་རང་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་སོང་།",
"outgoing_message_deleted_text": "", "incoming_message_deleted_text": "ཆ་འཕྲིན་འདི་བསུབས་ཟིན།",
"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": { "power_level": {
"moderator": "མདོ་འཛིན་པ།", "moderator": "མདོ་འཛིན་པ།",
@ -226,27 +256,24 @@
"text_public": "གལ་ཏེ་ཁྱེད་ཀྱིས་འབྲེལ་ཐག་དེ་ཧ་གོ་ཚེ། ག་དུས་ཡིན་ཡང་། ཁ་བརྡ་ཁང་དུ་འཛུལ་ཆོག", "text_public": "གལ་ཏེ་ཁྱེད་ཀྱིས་འབྲེལ་ཐག་དེ་ཧ་གོ་ཚེ། ག་དུས་ཡིན་ཡང་། ཁ་བརྡ་ཁང་དུ་འཛུལ་ཆོག",
"title_public": "{user} ག་ལེེར་བཞུགས།" "title_public": "{user} ག་ལེེར་བཞུགས།"
}, },
"logout": {
"confirm_text": ""
},
"join": { "join": {
"title": "ཁྱེད་རང་ནང་དུ་ཞུགས་པར་དགའ་བསུ་ཞུ།", "title": "ཁྱེད་རང་ནང་དུ་ཞུགས་པར་དགའ་བསུ་ཞུ།",
"title_user": "",
"user_name_label": "སྤྱོད་མིང་།", "user_name_label": "སྤྱོད་མིང་།",
"remember_me": "ང་དྲན་པར་བྱོས།", "remember_me": "ང་དྲན་པར་བྱོས།",
"joining_as": "ཁྱེད་རང་ཞུགས་བཞིན་པ།:", "joining_as": "ཁྱེད་རང་ཞུགས་བཞིན་པ།:",
"join": "ཁ་བརྡ་ཁང་དུ་འཛུལ།", "join": "ཁ་བརྡ་ཁང་དུ་འཛུལ།",
"join_user": "",
"enter_room": "ནང་དུ་ཞུགས།", "enter_room": "ནང་དུ་ཞུགས།",
"enter_room_user": "",
"status_logging_in": "ནང་འཛུལ་བྱེད་བཞིན་པ།...", "status_logging_in": "ནང་འཛུལ་བྱེད་བཞིན་པ།...",
"status_joining": "ཁ་བརྡ་ཁང་དུ་འཛུལ་བཞིན་པ།...", "status_joining": "ཁ་བརྡ་ཁང་དུ་འཛུལ་བཞིན་པ།...",
"join_failed": "ཁ་བརྡ་ཁང་དུ་འཛུལ་ཐུབ་མ་སོང་།", "join_failed": "ཁ་བརྡ་ཁང་དུ་འཛུལ་ཐུབ་མ་སོང་།",
"choose_name": "" "join_user": "ཁ་བརྡ་འགོ་རྩོམ།",
"enter_room_user": "ཁ་བརྡ་འགོ་རྩོམ།",
"choose_name": "མིང་ཞིག་འདེམས་ཏེ་བཀོལ།",
"title_user": "དགའ་བསུ་ཞུ། ཁྱེད་རང་ཁ་བརྡ་བྱེད་པར་གདན་ཞུ་གནང་སོང་།"
}, },
"profile_info_popup": { "profile_info_popup": {
"powered_by": "ཁ་བརྡ་ཁང་འདི་{product} ནུས་ཤུགས་བསྩལ་ཡོད། {productLink} ནས་དེ་ལས་མང་བ་སྦྱོང་ཆོག་ལ། མདུན་དུ་བསྐྱོད་དེ་ཁ་བརྡ་ཁང་གཞན་ཞིག་བསྐྲུན་ཆོག", "powered_by": "ཁ་བརྡ་ཁང་འདི་{product} ནུས་ཤུགས་བསྩལ་ཡོད། {productLink} ནས་དེ་ལས་མང་བ་སྦྱོང་ཆོག་ལ། མདུན་དུ་བསྐྱོད་དེ་ཁ་བརྡ་ཁང་གཞན་ཞིག་བསྐྲུན་ཆོག",
"new_room": "+ ཁ་བརྡ་ཁང་གསར་པ།", "new_room": "ཁ་བརྡ་ཁང་གསར་པ།",
"want_more": "དེ་ལས་མང་བ་དགོས་སམ།", "want_more": "དེ་ལས་མང་བ་དགོས་སམ།",
"logout": "ཕྱིར་ཐོན།", "logout": "ཕྱིར་ཐོན།",
"edit_profile": "སྒེར་གྱི་ཡིག་ཆ་བཅོས་སྒྲིག", "edit_profile": "སྒེར་གྱི་ཡིག་ཆ་བཅོས་སྒྲིག",
@ -259,5 +286,49 @@
"close_tab": "བཤེར་ཆས་ཁ་རྒྱོབས།", "close_tab": "བཤེར་ཆས་ཁ་རྒྱོབས།",
"room_deleted": "ཁ་བརྡ་ཁང་མེད་པར་བཟོས་སོང་།" "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}"
}
} }

View file

@ -1,9 +1,6 @@
{ {
"language_display_name": "Deutsch", "language_display_name": "Deutsch",
"global": { "global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
}, },
"menu": { "menu": {
"start_private_chat": "Private Diskussion mit diesem Benutzer", "start_private_chat": "Private Diskussion mit diesem Benutzer",
@ -59,9 +56,7 @@
"user_left": "{user} hat das Gespräch verlassen", "user_left": "{user} hat das Gespräch verlassen",
"user_joined": "{Benutzer} ist dem Gespräch beigetreten", "user_joined": "{Benutzer} ist dem Gespräch beigetreten",
"download_progress": "{percentage} % heruntergeladen", "download_progress": "{percentage} % heruntergeladen",
"user_changed_room_name": "{user} hat den Raumnamen in {name} geändert", "user_changed_room_name": "{user} hat den Raumnamen in {name} geändert"
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"room": { "room": {
"leave": "Verlassen", "leave": "Verlassen",
@ -71,9 +66,7 @@
"members": "keine Mitglieder | 1 Mitglied | {count} Mitglieder", "members": "keine Mitglieder | 1 Mitglied | {count} Mitglieder",
"purge_removing_members": "Entfernen von Mitgliedern", "purge_removing_members": "Entfernen von Mitgliedern",
"purge_failed": "Fehler beim Bereinigen des Raums!", "purge_failed": "Fehler beim Bereinigen des Raums!",
"room_list_rooms": "Räume", "room_list_rooms": "Räume"
"room_name_required": "",
"room_topic_required": ""
}, },
"room_welcome": { "room_welcome": {
"info": "Herzlich willkommen! Hier sind ein paar Dinge, die du über deinen Raum wissen solltest:", "info": "Herzlich willkommen! Hier sind ein paar Dinge, die du über deinen Raum wissen solltest:",
@ -103,9 +96,7 @@
"create": "Erstellen", "create": "Erstellen",
"next": "Nächste", "next": "Nächste",
"name_room": "Raum benennen", "name_room": "Raum benennen",
"room_topic": "Füge eine Beschreibung hinzu, wenn du möchtest", "room_topic": "Füge eine Beschreibung hinzu, wenn du möchtest"
"room_name_limit_error_msg": "",
"colon_not_allowed": ""
}, },
"device_list": { "device_list": {
"title": "GERÄTE", "title": "GERÄTE",
@ -121,8 +112,7 @@
"password_required": "Das Passwort ist erforderlich", "password_required": "Das Passwort ist erforderlich",
"login": "Anmelden", "login": "Anmelden",
"create_room": "Registrieren und Raum erstellen", "create_room": "Registrieren und Raum erstellen",
"or": "ODER", "or": "ODER"
"invalid_message": ""
}, },
"profile": { "profile": {
"title": "Mein Profil", "title": "Mein Profil",
@ -134,8 +124,7 @@
"password_old": "Altes Passwort", "password_old": "Altes Passwort",
"password_new": "Neues Kennwort", "password_new": "Neues Kennwort",
"password_repeat": "Wiederhole das neue Passwort", "password_repeat": "Wiederhole das neue Passwort",
"display_name": "Anzeigename", "display_name": "Anzeigename"
"display_name_required": ""
}, },
"profile_info_popup": { "profile_info_popup": {
"you_are": "Du bist", "you_are": "Du bist",
@ -156,8 +145,7 @@
"status_logging_in": "Wird angemeldet …", "status_logging_in": "Wird angemeldet …",
"status_joining": "Raum beitreten …", "status_joining": "Raum beitreten …",
"join_failed": "Beitritt zum Raum fehlgeschlagen.", "join_failed": "Beitritt zum Raum fehlgeschlagen.",
"title": "Willkommen in {roomName}", "title": "Willkommen in {roomName}"
"choose_name": ""
}, },
"invite": { "invite": {
"title": "Freunde hinzufügen", "title": "Freunde hinzufügen",
@ -177,7 +165,6 @@
"title_invite": "Bist du sicher, dass du gehen willst?" "title_invite": "Bist du sicher, dass du gehen willst?"
}, },
"logout": { "logout": {
"confirm_text": ""
}, },
"purge_room": { "purge_room": {
"info": "Alle Mitglieder und Nachrichten werden entfernt. Diese Aktion kann nicht rückgängig gemacht werden.", "info": "Alle Mitglieder und Nachrichten werden entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",

View file

@ -7,7 +7,11 @@
"global": { "global": {
"save": "Save", "save": "Save",
"password_didnot_match": "Password didn't match", "password_didnot_match": "Password didn't match",
"password_hint": "Minimum 12 character containing atleast one numeric, one uppercase and one lowercase letter" "password_hint": "Minimum 12 character containing 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": { "menu": {
"start_private_chat": "Private chat with this user", "start_private_chat": "Private chat with this user",
@ -54,6 +58,7 @@
"file_prefix": "File: ", "file_prefix": "File: ",
"edited": "(edited)", "edited": "(edited)",
"download_progress": "{percentage}% downloaded", "download_progress": "{percentage}% downloaded",
"upload_file_too_large": "File is too large to upload!",
"upload_progress": "Uploaded {count}", "upload_progress": "Uploaded {count}",
"upload_progress_with_total": "Uploaded {count} of {total}", "upload_progress_with_total": "Uploaded {count} of {total}",
"user_changed_room_history": "{user} made room history {type}", "user_changed_room_history": "{user} made room history {type}",
@ -82,10 +87,14 @@
"reply_poll": "Poll", "reply_poll": "Poll",
"time_ago": "Today | Yesterday | {count} days ago", "time_ago": "Today | Yesterday | {count} days ago",
"outgoing_message_deleted_text": "You deleted this message.", "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": { "room": {
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations", "invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",
"unseen_messages": "You have no unseen messages | You have 1 unseen message | You have {count} unseen messages",
"members": "no members | 1 member | {count} members", "members": "no members | 1 member | {count} members",
"leave": "Leave", "leave": "Leave",
"purge_set_room_state": "Setting room state", "purge_set_room_state": "Setting room state",
@ -93,6 +102,7 @@
"purge_removing_members": "Removing members ({count} of {total})", "purge_removing_members": "Removing members ({count} of {total})",
"purge_failed": "Failed to purge room!", "purge_failed": "Failed to purge room!",
"room_list_invites": "Invites", "room_list_invites": "Invites",
"room_list_new_messages": "{count} new messages",
"room_list_rooms": "Rooms", "room_list_rooms": "Rooms",
"room_name_required": "Room name is required", "room_name_required": "Room name is required",
"room_topic_required": "Room description is required" "room_topic_required": "Room description is required"
@ -105,7 +115,8 @@
"join_public": "Anyone can join by opening this link: {link}.", "join_public": "Anyone can join by opening this link: {link}.",
"join_invite": "Only people you invite can join.", "join_invite": "Only people you invite can join.",
"info_permissions": "You can change join permissions at any time in the room settings.", "info_permissions": "You can change join permissions at any time in the room settings.",
"got_it": "Got it" "got_it": "Got it",
"no_past_messages": "Welcome! For your security, past messages are not available."
}, },
"new_room": { "new_room": {
"new_room": "New Room", "new_room": "New Room",
@ -145,7 +156,15 @@
"login": "Login", "login": "Login",
"create_room": "Register & Create Room", "create_room": "Register & Create Room",
"or": "OR", "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": { "profile": {
"title": "My Profile", "title": "My Profile",
@ -172,7 +191,7 @@
"logout": "Logout", "logout": "Logout",
"want_more": "Want more?", "want_more": "Want more?",
"powered_by": "This room is powered by {product}. Learn more at {productLink} or go ahead and create another room!", "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": { "join": {
"title": "Welcome you have been invited to join", "title": "Welcome you have been invited to join",
@ -246,7 +265,12 @@
"user_moderator": "Moderator", "user_moderator": "Moderator",
"experimental_features": "Experimental Features", "experimental_features": "Experimental Features",
"voice_mode": "Voice mode", "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": { "room_info_sheet": {
"this_room": "This room", "this_room": "This room",

View file

@ -5,9 +5,6 @@
"tag_line": "Simplemente conectar" "tag_line": "Simplemente conectar"
}, },
"global": { "global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
}, },
"room_info": { "room_info": {
"identity": "Has iniciado sesión como {displayName}.", "identity": "Has iniciado sesión como {displayName}.",
@ -53,7 +50,6 @@
"title_public": "Adios, [user}" "title_public": "Adios, [user}"
}, },
"logout": { "logout": {
"confirm_text": ""
}, },
"invite": { "invite": {
"status_error": "No se pudo invitar a uno o más amigos!", "status_error": "No se pudo invitar a uno o más amigos!",
@ -64,18 +60,14 @@
}, },
"join": { "join": {
"title": "Bienvenido has sido invitado a unirte", "title": "Bienvenido has sido invitado a unirte",
"title_user": "",
"user_name_label": "Nombre de usuario", "user_name_label": "Nombre de usuario",
"remember_me": "Recordarme", "remember_me": "Recordarme",
"joining_as": "Te estas uniendo como:", "joining_as": "Te estas uniendo como:",
"join": "Unirse a la sala", "join": "Unirse a la sala",
"join_user": "",
"enter_room": "Entrar habitacion", "enter_room": "Entrar habitacion",
"enter_room_user": "",
"status_logging_in": "Iniciando sesión...", "status_logging_in": "Iniciando sesión...",
"status_joining": "Uniendose a la sala...", "status_joining": "Uniendose a la sala...",
"join_failed": "No se pudo unir a la sala.", "join_failed": "No se pudo unir a la sala."
"choose_name": ""
}, },
"profile": { "profile": {
"display_name": "Nombre para mostrar", "display_name": "Nombre para mostrar",
@ -91,8 +83,7 @@
"set_language": "Establece tu Idioma", "set_language": "Establece tu Idioma",
"language_description": "Convine esta disponible en varios Idiomas.", "language_description": "Convine esta disponible en varios Idiomas.",
"dont_see_yours": "¿No ves el tuyo?", "dont_see_yours": "¿No ves el tuyo?",
"tell_us": "Dinos.", "tell_us": "Dinos."
"display_name_required": ""
}, },
"login": { "login": {
"login": "Iniciar sesión", "login": "Iniciar sesión",
@ -130,9 +121,7 @@
"done": "Listo", "done": "Listo",
"new_room": "Nueva Sala", "new_room": "Nueva Sala",
"create": "Crear", "create": "Crear",
"room_topic": "Añade una descripción si quieres", "room_topic": "Añade una descripción si quieres"
"room_name_limit_error_msg": "",
"colon_not_allowed": ""
}, },
"room_welcome": { "room_welcome": {
"join_public": "Cualquiera puede unirse abriendo este vínculo: {link}", "join_public": "Cualquiera puede unirse abriendo este vínculo: {link}",
@ -153,9 +142,7 @@
"purge_set_room_state": "Estado de la sala", "purge_set_room_state": "Estado de la sala",
"purge_removing_members": "Eliminar miembros", "purge_removing_members": "Eliminar miembros",
"purge_failed": "¡Fallo en la purga de la sala!", "purge_failed": "¡Fallo en la purga de la sala!",
"room_list_rooms": "Salas", "room_list_rooms": "Salas"
"room_name_required": "",
"room_topic_required": ""
}, },
"message": { "message": {
"user_powerlevel_change_from_to": "{user} de {powerOld} a {powerNew}", "user_powerlevel_change_from_to": "{user} de {powerOld} a {powerNew}",
@ -195,11 +182,8 @@
"reply_image": "Imagen", "reply_image": "Imagen",
"reply_audio_message": "Mensaje de audio", "reply_audio_message": "Mensaje de audio",
"reply_video": "Vídeo", "reply_video": "Vídeo",
"reply_poll": "",
"user_changed_guest_access_closed": "{user} no has permitido que los invitados se unan a la sala", "user_changed_guest_access_closed": "{user} no has permitido que los invitados se unan a la sala",
"user_changed_guest_access_open": "{user} has permitido que los invitados se unieran a la sala", "user_changed_guest_access_open": "{user} has permitido que los invitados se unieran a la sala"
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"menu": { "menu": {
"login": "Iniciar sesión", "login": "Iniciar sesión",

View file

@ -1,8 +1,5 @@
{ {
"global": { "global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
}, },
"menu": { "menu": {
"back": "TAKAISIN", "back": "TAKAISIN",
@ -32,9 +29,7 @@
"room_topic": "Lisää kuvaus, jos haluat", "room_topic": "Lisää kuvaus, jos haluat",
"add_people": "Lisää ihmisiä", "add_people": "Lisää ihmisiä",
"link_copied": "Linkki kopioitu!", "link_copied": "Linkki kopioitu!",
"public_info": "Kuka tahansa, jolla on linkki", "public_info": "Kuka tahansa, jolla on linkki"
"room_name_limit_error_msg": "",
"colon_not_allowed": ""
}, },
"purge_room": { "purge_room": {
"n_seconds": "{seconds} sekuntia", "n_seconds": "{seconds} sekuntia",
@ -67,23 +62,16 @@
"login": "Kirjaudu sisään", "login": "Kirjaudu sisään",
"password": "Anna salasana", "password": "Anna salasana",
"password_required": "Salasana vaaditaan", "password_required": "Salasana vaaditaan",
"create_room": "Rekisteröidy ja luo huone", "create_room": "Rekisteröidy ja luo huone"
"invalid_message": ""
}, },
"join": { "join": {
"title": "Tervetuloa huoneen {roomName}", "title": "Tervetuloa huoneen {roomName}",
"title_user": "",
"user_name_label": "Käyttäjätunnus", "user_name_label": "Käyttäjätunnus",
"remember_me": "",
"joining_as": "Liityt jäsenenä:", "joining_as": "Liityt jäsenenä:",
"join": "Liity huoneeseen", "join": "Liity huoneeseen",
"join_user": "",
"enter_room": "",
"enter_room_user": "",
"status_logging_in": "Kirjautuminen sisään…", "status_logging_in": "Kirjautuminen sisään…",
"status_joining": "Liittyminen huoneeseen…", "status_joining": "Liittyminen huoneeseen…",
"join_failed": "Huoneeseen liittyminen epäonnistui.", "join_failed": "Huoneeseen liittyminen epäonnistui."
"choose_name": ""
}, },
"leave": { "leave": {
"title_public": "Näkemiin, {user}", "title_public": "Näkemiin, {user}",
@ -94,7 +82,6 @@
"text_invite": "Tämä huone on lukittu. Et pääse takaisin ilman erillistä lupaa." "text_invite": "Tämä huone on lukittu. Et pääse takaisin ilman erillistä lupaa."
}, },
"logout": { "logout": {
"confirm_text": ""
}, },
"message": { "message": {
"you": "Sinä", "you": "Sinä",
@ -109,15 +96,11 @@
"user_joined": "{user} liittyi keskusteluun", "user_joined": "{user} liittyi keskusteluun",
"file_prefix": "Tiedosto: ", "file_prefix": "Tiedosto: ",
"edited": "(muokattu)", "edited": "(muokattu)",
"users_are_typing": "{count} jäsentä kirjoitavat", "users_are_typing": "{count} jäsentä kirjoitavat"
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"room": { "room": {
"leave": "Poistu", "leave": "Poistu",
"room_list_rooms": "Huoneet", "room_list_rooms": "Huoneet"
"room_name_required": "",
"room_topic_required": ""
}, },
"room_welcome": { "room_welcome": {
"room_history_is": "Huoneen historia on {type}.", "room_history_is": "Huoneen historia on {type}.",
@ -136,8 +119,7 @@
"set_password": "Aseta salasana", "set_password": "Aseta salasana",
"select_language": "Kieli", "select_language": "Kieli",
"password_old": "Vanha salasana", "password_old": "Vanha salasana",
"display_name": "Näyttönimi", "display_name": "Näyttönimi"
"display_name_required": ""
}, },
"profile_info_popup": { "profile_info_popup": {
"want_more": "Haluatko lisää?", "want_more": "Haluatko lisää?",

View file

@ -1,8 +1,5 @@
{ {
"global": { "global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
}, },
"menu": { "menu": {
"edit": "Modifier", "edit": "Modifier",
@ -59,9 +56,7 @@
"room_joinrule_public": "public", "room_joinrule_public": "public",
"unread_messages": "Messages non lus", "unread_messages": "Messages non lus",
"users_are_typing": "{count} membres écrivent", "users_are_typing": "{count} membres écrivent",
"room_powerlevel_change": "{user} a changé le statut de {changes}", "room_powerlevel_change": "{user} a changé le statut de {changes}"
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"room": { "room": {
"members": "aucun membre | 1 membre | {count} membres", "members": "aucun membre | 1 membre | {count} membres",
@ -71,9 +66,7 @@
"purge_removing_members": "Suppression de membres", "purge_removing_members": "Suppression de membres",
"room_list_invites": "Invitations", "room_list_invites": "Invitations",
"room_list_rooms": "Salons", "room_list_rooms": "Salons",
"purge_redacting_events": "Rédaction des évènements", "purge_redacting_events": "Rédaction des évènements"
"room_name_required": "",
"room_topic_required": ""
}, },
"room_welcome": { "room_welcome": {
"info": "Bienvenue ! Voici quelques informations à connaître sur votre salon :", "info": "Bienvenue ! Voici quelques informations à connaître sur votre salon :",
@ -103,9 +96,7 @@
"get_link": "Obtenir le lien", "get_link": "Obtenir le lien",
"public_info": "Quiconque avec un 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é dautres personnes peuvent être invitées. Elles peuvent être modifiées à tout moment.", "join_permissions_info": "Ces autorisations déterminent comment les personnes peuvent rejoindre le salon et avec quelle facilité dautres personnes peuvent être invitées. Elles peuvent être modifiées à tout moment.",
"status_creating": "Création du salon", "status_creating": "Création du salon"
"room_name_limit_error_msg": "",
"colon_not_allowed": ""
}, },
"device_list": { "device_list": {
"title": "APPAREILS", "title": "APPAREILS",
@ -121,8 +112,7 @@
"password_required": "Le mot de passe est obligatoire", "password_required": "Le mot de passe est obligatoire",
"create_room": "Sinscrire et créer un salon", "create_room": "Sinscrire et créer un salon",
"or": "OU", "or": "OU",
"login": "Se connecter", "login": "Se connecter"
"invalid_message": ""
}, },
"profile": { "profile": {
"temporary_identity": "Cette identité est temporaire. Définissez un mot de passe pour lutiliser à nouveau", "temporary_identity": "Cette identité est temporaire. Définissez un mot de passe pour lutiliser à nouveau",
@ -134,8 +124,7 @@
"display_name": "Nom daffichage", "display_name": "Nom daffichage",
"title": "Mon profil", "title": "Mon profil",
"set_password": "Définir un mot de passe", "set_password": "Définir un mot de passe",
"password_new": "Nouveau mot de passe", "password_new": "Nouveau mot de passe"
"display_name_required": ""
}, },
"profile_info_popup": { "profile_info_popup": {
"you_are": "Vous êtes", "you_are": "Vous êtes",
@ -149,18 +138,12 @@
}, },
"join": { "join": {
"title": "Bienvenue dans {roomName}", "title": "Bienvenue dans {roomName}",
"title_user": "",
"user_name_label": "Nom dutilisateur", "user_name_label": "Nom dutilisateur",
"remember_me": "",
"joining_as": "Vous rejoignez en tant que :", "joining_as": "Vous rejoignez en tant que :",
"join": "Rejoindre le salon", "join": "Rejoindre le salon",
"join_user": "",
"enter_room": "",
"enter_room_user": "",
"status_logging_in": "Connexion en cours…", "status_logging_in": "Connexion en cours…",
"status_joining": "Adhésion au salon…", "status_joining": "Adhésion au salon…",
"join_failed": "Impossible de rejoindre le salon.", "join_failed": "Impossible de rejoindre le salon."
"choose_name": ""
}, },
"invite": { "invite": {
"title": "Ajouter des amis", "title": "Ajouter des amis",
@ -180,7 +163,6 @@
"go_back": "Retour" "go_back": "Retour"
}, },
"logout": { "logout": {
"confirm_text": ""
}, },
"purge_room": { "purge_room": {
"title": "Supprimer le salon ?", "title": "Supprimer le salon ?",

View file

@ -1,8 +1,5 @@
{ {
"global": { "global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
}, },
"message": { "message": {
"file_prefix": "File: ", "file_prefix": "File: ",
@ -40,9 +37,7 @@
"room_history_world_readable": "leggibile da chiunque", "room_history_world_readable": "leggibile da chiunque",
"room_history_shared": "leggibile da tutti i membri nella stanza", "room_history_shared": "leggibile da tutti i membri nella stanza",
"user_is_typing": "{user} sta scrivendo", "user_is_typing": "{user} sta scrivendo",
"users_are_typing": "{count} membri stanno scrivendo", "users_are_typing": "{count} membri stanno scrivendo"
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"room": { "room": {
"purge_removing_members": "Rimozione di membri", "purge_removing_members": "Rimozione di membri",
@ -52,9 +47,7 @@
"purge_redacting_events": "Redazione di eventi", "purge_redacting_events": "Redazione di eventi",
"purge_failed": "Impossibile pulire la stanza!", "purge_failed": "Impossibile pulire la stanza!",
"room_list_invites": "Inviti", "room_list_invites": "Inviti",
"room_list_rooms": "Stanze", "room_list_rooms": "Stanze"
"room_name_required": "",
"room_topic_required": ""
}, },
"menu": { "menu": {
"reply": "Risposta", "reply": "Risposta",
@ -102,9 +95,7 @@
"new_room": "Nuova stanza", "new_room": "Nuova stanza",
"invite_info": "Solo le persone aggiunte", "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.", "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", "public_info": "Chiunque abbia un collegamento"
"room_name_limit_error_msg": "",
"colon_not_allowed": ""
}, },
"device_list": { "device_list": {
"title": "DISPOSITIVI", "title": "DISPOSITIVI",
@ -120,8 +111,7 @@
"login": "Accedi", "login": "Accedi",
"create_room": "Registrati e crea una stanza", "create_room": "Registrati e crea una stanza",
"or": "O", "or": "O",
"username_required": "Il nome utente è richiesto", "username_required": "Il nome utente è richiesto"
"invalid_message": ""
}, },
"profile": { "profile": {
"title": "Il mio profilo", "title": "Il mio profilo",
@ -133,8 +123,7 @@
"display_name": "Nome visualizzato", "display_name": "Nome visualizzato",
"change_name": "Cambia il nome", "change_name": "Cambia il nome",
"change_password": "Cambia la password", "change_password": "Cambia la password",
"password_new": "Nuova password", "password_new": "Nuova password"
"display_name_required": ""
}, },
"profile_info_popup": { "profile_info_popup": {
"you_are": "Sei", "you_are": "Sei",
@ -148,18 +137,12 @@
}, },
"join": { "join": {
"title": "Benvenuto/a in {roomName}", "title": "Benvenuto/a in {roomName}",
"title_user": "",
"user_name_label": "Nome utente", "user_name_label": "Nome utente",
"remember_me": "",
"joining_as": "Ti stai unendo come:", "joining_as": "Ti stai unendo come:",
"join": "Unisciti alla stanza", "join": "Unisciti alla stanza",
"join_user": "",
"enter_room": "",
"enter_room_user": "",
"status_logging_in": "Accesso in corso…", "status_logging_in": "Accesso in corso…",
"status_joining": "Unendosi alla stanza…", "status_joining": "Unendosi alla stanza…",
"join_failed": "Impossibile unirsi alla stanza.", "join_failed": "Impossibile unirsi alla stanza."
"choose_name": ""
}, },
"invite": { "invite": {
"title": "Aggiungi amici", "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}." "text_public_lastroom": "Se vuoi unirti di nuovo a questa stanza, puoi farlo con una nuova identità. Per mantenere {user}, {action}."
}, },
"logout": { "logout": {
"confirm_text": ""
}, },
"purge_room": { "purge_room": {
"info": "Tutti i membri e i messaggi saranno rimossi. Questa azione non può essere annullata.", "info": "Tutti i membri e i messaggi saranno rimossi. Questa azione non può essere annullata.",

View file

@ -1,12 +1,8 @@
{ {
"project": { "project": {
"name": "Convene", "name": "Convene"
"tag_line": ""
}, },
"global": { "global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
}, },
"message": { "message": {
"user_changed_guest_access_open": "{user} tillot gjester å ta del i rommet", "user_changed_guest_access_open": "{user} tillot gjester å ta del i rommet",
@ -33,13 +29,7 @@
"file_prefix": "Fil: ", "file_prefix": "Fil: ",
"user_said": "{user} sa:", "user_said": "{user} sa:",
"user_created_room": "{user} opprettet rommet", "user_created_room": "{user} opprettet rommet",
"you": "Deg", "you": "Deg"
"reply_image": "",
"reply_audio_message": "",
"reply_video": "",
"reply_poll": "",
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"device_list": { "device_list": {
"title": "Enheter", "title": "Enheter",
@ -80,18 +70,9 @@
}, },
"join": { "join": {
"title": "Velkommen til {roomName}", "title": "Velkommen til {roomName}",
"title_user": "",
"user_name_label": "",
"remember_me": "",
"joining_as": "",
"join": "Ta del i rom", "join": "Ta del i rom",
"join_user": "",
"enter_room": "",
"enter_room_user": "",
"status_logging_in": "Logger inn …", "status_logging_in": "Logger inn …",
"status_joining": "Tar del i rom…", "status_joining": "Tar del i rom…"
"join_failed": "",
"choose_name": ""
}, },
"profile_info_popup": { "profile_info_popup": {
"identity_temporary": "{displayName}", "identity_temporary": "{displayName}",
@ -104,35 +85,25 @@
"password_new": "Nytt passord", "password_new": "Nytt passord",
"password_old": "Gammelt passord", "password_old": "Gammelt passord",
"select_language": "Språk", "select_language": "Språk",
"set_language": "",
"language_description": "",
"dont_see_yours": "",
"tell_us": "",
"change_password": "Endre passord", "change_password": "Endre passord",
"change_name": "Endre navn", "change_name": "Endre navn",
"set_password": "Sett passord", "set_password": "Sett passord",
"title": "Min profil", "title": "Min profil",
"display_name": "Visningsnavn", "display_name": "Visningsnavn",
"password_repeat": "Gjenta nytt passord", "password_repeat": "Gjenta nytt passord"
"display_name_required": ""
}, },
"login": { "login": {
"password_required": "Passord kreves", "password_required": "Passord kreves",
"username_required": "Brukernavn kreves", "username_required": "Brukernavn kreves",
"password": "Passord", "password": "Passord",
"username": "Brukernavn", "username": "Brukernavn"
"create_room": "",
"or": "",
"invalid_message": ""
}, },
"new_room": { "new_room": {
"add_people": "Legg til folk", "add_people": "Legg til folk",
"link_copied": "Lenke kopiert.", "link_copied": "Lenke kopiert.",
"next": "Neste", "next": "Neste",
"create": "Opprett", "create": "Opprett",
"new_room": "Nytt rom", "new_room": "Nytt rom"
"room_name_limit_error_msg": "",
"colon_not_allowed": ""
}, },
"room_welcome": { "room_welcome": {
"room_history_is": "Romhistorikken er {type}.", "room_history_is": "Romhistorikken er {type}.",
@ -144,9 +115,7 @@
"room_list_rooms": "Rom", "room_list_rooms": "Rom",
"room_list_invites": "Invitasjoner", "room_list_invites": "Invitasjoner",
"purge_set_room_state": "Setter romtilstand", "purge_set_room_state": "Setter romtilstand",
"leave": "Forlat", "leave": "Forlat"
"room_name_required": "",
"room_topic_required": ""
}, },
"purge_room": { "purge_room": {
"n_seconds": "{seconds} sekunder", "n_seconds": "{seconds} sekunder",
@ -159,7 +128,6 @@
"title_public": "Adjø, {user}" "title_public": "Adjø, {user}"
}, },
"logout": { "logout": {
"confirm_text": ""
}, },
"invite": { "invite": {
"status_inviting": "Inviterer venn {index} av {count}", "status_inviting": "Inviterer venn {index} av {count}",
@ -176,7 +144,6 @@
"login": "Logg inn", "login": "Logg inn",
"send": "Send", "send": "Send",
"ok": "OK", "ok": "OK",
"done": "",
"cancel": "Avbryt", "cancel": "Avbryt",
"download": "last ned", "download": "last ned",
"delete": "Slett", "delete": "Slett",

View file

@ -4,9 +4,13 @@
"tag_line": "Basta conectar" "tag_line": "Basta conectar"
}, },
"global": { "global": {
"save": "", "save": "Salvar",
"password_didnot_match": "", "password_didnot_match": "A senha não coincidiu",
"password_hint": "" "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": { "invite": {
"title": "Adiciona amigos", "title": "Adiciona amigos",
@ -39,7 +43,12 @@
"undo": "Desfazer", "undo": "Desfazer",
"join": "Entrar", "join": "Entrar",
"ignore": "Ignore", "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": { "message": {
"you": "Você", "you": "Você",
@ -81,23 +90,33 @@
"reply_image": "Imagem", "reply_image": "Imagem",
"reply_audio_message": "Mensagem de áudio", "reply_audio_message": "Mensagem de áudio",
"reply_video": "Vídeo", "reply_video": "Vídeo",
"reply_poll": "", "reply_poll": "Enquete",
"time_ago": "Hoje | Ontem | {count} dias atrás", "time_ago": "Hoje | Ontem | {count} dias atrás",
"outgoing_message_deleted_text": "", "outgoing_message_deleted_text": "Você excluiu esta mensagem.",
"incoming_message_deleted_text": "" "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": { "room": {
"members": "sem membros | 1 membro | {count} membros", "members": "sem membros | 1 membro | {count} membros",
"leave": "Sair", "leave": "Sair",
"purge_set_room_state": "Configurando o estado da sala", "purge_set_room_state": "Configurando o estado da sala",
"purge_redacting_events": "Redigindo os eventos", "purge_redacting_events": "Redigindo eventos ({count} de {total})",
"purge_removing_members": "Removendo os membros", "purge_removing_members": "Removendo membros ({count} de {total})",
"purge_failed": "Houve uma falha ao eliminar a sala!", "purge_failed": "Houve uma falha ao eliminar a sala!",
"room_list_invites": "Convites", "room_list_invites": "Convites",
"room_list_rooms": "Salas", "room_list_rooms": "Salas",
"invitations": "Você não tem convites | Você tem 1 convite | Você tem {count} convites", "invitations": "Você não tem convites | Você tem 1 convite | Você tem {count} convites",
"room_name_required": "", "room_name_required": "O nome da sala é obrigatório",
"room_topic_required": "" "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": { "room_welcome": {
"info": "Bem-vindo! Aqui estão algumas coisas que você deve saber sobre a sua sala:", "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_total": "Enviando o avatar: {count} de {total}",
"status_avatar": "Enviando avatar: {count}", "status_avatar": "Enviando avatar: {count}",
"room_name_limit_error_msg": "O máximo de 50 caracteres são permitidos", "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": { "device_list": {
"title": "DISPOSITIVOS", "title": "DISPOSITIVOS",
@ -146,7 +166,15 @@
"login": "Entrar", "login": "Entrar",
"create_room": "Cadastre-se e crie uma sala", "create_room": "Cadastre-se e crie uma sala",
"or": "OU", "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": { "profile": {
"title": "Meu perfil", "title": "Meu perfil",
@ -163,7 +191,7 @@
"password_new": "Nova senha", "password_new": "Nova senha",
"password_repeat": "Repita a nova senha", "password_repeat": "Repita a nova senha",
"display_name": "Nome de exibição", "display_name": "Nome de exibição",
"display_name_required": "" "display_name_required": "O nome de exibição é obrigatório"
}, },
"profile_info_popup": { "profile_info_popup": {
"you_are": "Você é", "you_are": "Você é",
@ -173,18 +201,18 @@
"logout": "Sair", "logout": "Sair",
"want_more": "Quer mais?", "want_more": "Quer mais?",
"powered_by": "Esta sala é oferecida por {product}. Saiba mais em {productLink} ou vá em frente e crie outra sala!", "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": { "join": {
"title": "Bem-vindo, você foi convidado a participar", "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", "user_name_label": "Nome do usuário",
"remember_me": "Me lembre", "remember_me": "Me lembre",
"joining_as": "Você está entrando como:", "joining_as": "Você está entrando como:",
"join": "Entrar na sala", "join": "Entrar na sala",
"join_user": "", "join_user": "Comece a conversar",
"enter_room": "Entre na sala", "enter_room": "Entre na sala",
"enter_room_user": "", "enter_room_user": "Comece a conversar",
"status_logging_in": "Fazendo login...", "status_logging_in": "Fazendo login...",
"status_joining": "Entrando na sala...", "status_joining": "Entrando na sala...",
"join_failed": "Houve uma falha ao entrar na sala.", "join_failed": "Houve uma falha ao entrar na sala.",
@ -201,7 +229,7 @@
"leave": "Sair" "leave": "Sair"
}, },
"logout": { "logout": {
"confirm_text": "" "confirm_text": "Tem certeza de que deseja sair?"
}, },
"purge_room": { "purge_room": {
"title": "Exclui a sala?", "title": "Exclui a sala?",
@ -235,7 +263,15 @@
"leave_room": "Sair", "leave_room": "Sair",
"version_info": "Desenvolvido por Guardian Project. Versão: {version}", "version_info": "Desenvolvido por Guardian Project. Versão: {version}",
"scan_code": "Faça a varredura para entrar na sala", "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": { "room_info_sheet": {
"this_room": "Esta sala", "this_room": "Esta sala",
@ -256,14 +292,14 @@
"restricted": "restrito" "restricted": "restrito"
}, },
"poll_create": { "poll_create": {
"title": "Criar uma enquete", "title": "Criar uma nova enquete",
"intro": "Preencha os detalhes abaixo.", "intro": "Preencha os detalhes abaixo.",
"create": "Criar", "create": "Publicar",
"creating": "Criando a enquete", "creating": "Criando a enquete",
"poll_disclosed": "Aberto - os resultados atuais são mostrados em todos os momentos.", "poll_disclosed": "Aberto - os resultados atuais são mostrados em todos os momentos.",
"add_answer": "Adicionar uma resposta", "add_answer": "Adicionar uma resposta",
"failed": "Houve uma falha ao criar a enquete. Tente novamente mais tarde.", "failed": "Houve uma falha ao criar a enquete. Tente novamente mais tarde.",
"question_label": "Digite a sua pergunta aqui", "question_label": "Faça a sua pergunta*",
"question_required": "Você precisa inserir uma pergunta!", "question_required": "Você precisa inserir uma pergunta!",
"answer_label": "Responda sem {index}", "answer_label": "Responda sem {index}",
"answer_required": "A resposta não pode estar vazia. Insira algum texto ou remova esta opção.", "answer_required": "A resposta não pode estar vazia. Insira algum texto ou remova esta opção.",
@ -274,8 +310,15 @@
"poll_status_open_not_voted": "A enquete está aberta - vote para ver os resultados", "poll_status_open_not_voted": "A enquete está aberta - vote para ver os resultados",
"close_poll": "Fechar a enquete", "close_poll": "Fechar a enquete",
"poll_submit": "Enviar", "poll_submit": "Enviar",
"num_answered": "{count} responderam", "num_answered": "{count} respostas",
"poll_undisclosed": "Fechado - os usuários verão os resultados quando a pesquisa for fechada." "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": { "export": {
"exported_date": "Foi exportado em {date}", "exported_date": "Foi exportado em {date}",

View file

@ -4,9 +4,6 @@
"tag_line": "Conectați pur și simplu" "tag_line": "Conectați pur și simplu"
}, },
"global": { "global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
}, },
"menu": { "menu": {
"ok": "OK", "ok": "OK",
@ -89,7 +86,6 @@
"title_public": "La revedere, {user}" "title_public": "La revedere, {user}"
}, },
"logout": { "logout": {
"confirm_text": ""
}, },
"invite": { "invite": {
"status_error": "Nu ați reușit să invitați unul sau mai mulți prieteni!", "status_error": "Nu ați reușit să invitați unul sau mai mulți prieteni!",
@ -100,18 +96,14 @@
}, },
"join": { "join": {
"title": "Bine ați venit, ați fost invitat să vă alăturați", "title": "Bine ați venit, ați fost invitat să vă alăturați",
"title_user": "",
"user_name_label": "Numele utilizatorului", "user_name_label": "Numele utilizatorului",
"remember_me": "Amintește-ți de mine", "remember_me": "Amintește-ți de mine",
"joining_as": "Vă înscrieți ca:", "joining_as": "Vă înscrieți ca:",
"join": "Alăturați-vă camerei", "join": "Alăturați-vă camerei",
"join_user": "",
"enter_room": "Intră în cameră", "enter_room": "Intră în cameră",
"enter_room_user": "",
"status_logging_in": "Autentificare...", "status_logging_in": "Autentificare...",
"status_joining": "Intrarea în cameră...", "status_joining": "Intrarea în cameră...",
"join_failed": "Nu a reușit să intre în cameră.", "join_failed": "Nu a reușit să intre în cameră."
"choose_name": ""
}, },
"profile_info_popup": { "profile_info_popup": {
"new_room": "+ Cameră nouă", "new_room": "+ Cameră nouă",
@ -137,8 +129,7 @@
"change_name": "Schimbă numele", "change_name": "Schimbă numele",
"set_password": "Setați parola", "set_password": "Setați parola",
"temporary_identity": "Această identitate este temporară. Setați o parolă pentru a o utiliza din nou", "temporary_identity": "Această identitate este temporară. Setați o parolă pentru a o utiliza din nou",
"title": "Profilul meu", "title": "Profilul meu"
"display_name_required": ""
}, },
"login": { "login": {
"login": "Autentificare", "login": "Autentificare",
@ -175,9 +166,7 @@
"name_room": "Nume cameră", "name_room": "Nume cameră",
"next": "Următorul", "next": "Următorul",
"create": "Creați", "create": "Creați",
"new_room": "Cameră nouă", "new_room": "Cameră nouă"
"room_name_limit_error_msg": "",
"colon_not_allowed": ""
}, },
"room_welcome": { "room_welcome": {
"got_it": "L-am prins", "got_it": "L-am prins",
@ -197,9 +186,7 @@
"purge_redacting_events": "Redactarea evenimentelor", "purge_redacting_events": "Redactarea evenimentelor",
"purge_set_room_state": "Setarea stării camerei", "purge_set_room_state": "Setarea stării camerei",
"leave": "Lăsați", "leave": "Lăsați",
"members": "fără membri | 1 membru | {count} membri", "members": "fără membri | 1 membru | {count} membri"
"room_name_required": "",
"room_topic_required": ""
}, },
"message": { "message": {
"user_changed_guest_access_open": "{user} a permis oaspeților să intre în cameră", "user_changed_guest_access_open": "{user} a permis oaspeților să intre în cameră",
@ -240,10 +227,7 @@
"you": "Tu", "you": "Tu",
"reply_image": "Imagine", "reply_image": "Imagine",
"reply_audio_message": "Mesaj audio", "reply_audio_message": "Mesaj audio",
"reply_video": "Videoclip", "reply_video": "Videoclip"
"reply_poll": "",
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"language_display_name": "Engleză", "language_display_name": "Engleză",
"fallbacks": { "fallbacks": {

View file

@ -1,11 +1,9 @@
{ {
"project": { "project": {
"name": "Convene", "name": "Convene"
"tag_line": ""
}, },
"menu": { "menu": {
"ok": "හරි", "ok": "හරි",
"done": "",
"download": "බාගන්න", "download": "බාගන්න",
"edit": "සංස්කරණය", "edit": "සංස්කරණය",
"reply": "පිලිතුර", "reply": "පිලිතුර",
@ -22,33 +20,12 @@
"message": { "message": {
"download_progress": "{percentage}% බාගත වී ඇත", "download_progress": "{percentage}% බාගත වී ඇත",
"file_prefix": "ගොනුව: ", "file_prefix": "ගොනුව: ",
"reply_image": "", "you": "ඔබ"
"reply_audio_message": "",
"reply_video": "",
"reply_poll": "",
"you": "ඔබ",
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"login": { "login": {
"invalid_message": ""
}, },
"join": { "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": { "logout": {
"confirm_text": ""
} }
} }

View file

@ -1,17 +1,12 @@
{ {
"project": { "project": {
"name": "Convene", "name": "Convene"
"tag_line": ""
}, },
"global": { "global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
}, },
"language_is_rtl": true, "language_is_rtl": true,
"menu": { "menu": {
"ok": "تامام", "ok": "تامام",
"done": "",
"download": "چۈشۈرۈش", "download": "چۈشۈرۈش",
"delete": "ئۆچۈرۈش", "delete": "ئۆچۈرۈش",
"edit": "تەھرىر", "edit": "تەھرىر",
@ -29,12 +24,12 @@
"start_private_chat": "قوللانغۇچى بىلەن شەخسى ئۇچۇرلاشماق" "start_private_chat": "قوللانغۇچى بىلەن شەخسى ئۇچۇرلاشماق"
}, },
"message": { "message": {
"upload_progress_with_total": "{ئومۇمىي} نىڭ {سان} يۈكلەندى", "upload_progress_with_total": "{number} نىڭ {total} يۈكلەندى",
"upload_progress": "يۈكلەندى {سان}", "upload_progress": "يۈكلەندى {count}",
"download_progress": "{پىرسەنت}% چۈشۈرۈلدى", "download_progress": "{percentage}% چۈشۈرۈلدى",
"edited": "تەھرىرلەندى", "edited": "تەھرىرلەندى",
"file_prefix": "ھۆججەت ", "file_prefix": "ھۆججەت ",
"user_said": "قوللانغۇچى سۆزلىدى:", "user_said": "{user} سۆزلىدى:",
"user_left": "قوللانغۇچى مۇنازىرىدىن چېكىندى", "user_left": "قوللانغۇچى مۇنازىرىدىن چېكىندى",
"user_joined": "قوللانغۇچى مۇنازىرىغا قاتناشتى", "user_joined": "قوللانغۇچى مۇنازىرىغا قاتناشتى",
"user_was_invited": "قوللانغۇچى مۇنازىرە ئۆيىگە تەكلىپ قىلىندى...", "user_was_invited": "قوللانغۇچى مۇنازىرە ئۆيىگە تەكلىپ قىلىندى...",
@ -45,12 +40,12 @@
"user_aliased_room": "مۇنازىرە ئۆيىنىڭ ئىسمى ئۆزگەرتىلدى", "user_aliased_room": "مۇنازىرە ئۆيىنىڭ ئىسمى ئۆزگەرتىلدى",
"user_created_room": "يېڭى مۇنازىرە ئۆيى قۇرۇلدى", "user_created_room": "يېڭى مۇنازىرە ئۆيى قۇرۇلدى",
"you": "سىز", "you": "سىز",
"room_powerlevel_change": "{قوللانغۇچى} دەرىجىسىنى ئۆزگەرتىش {ئۆزگەرتىش}", "room_powerlevel_change": "{user} دەرىجىسىنى ئۆزگەرتىش {changes}",
"user_changed_guest_access_open": "قوللانغۇچى ئەزالارنىڭ مۇنازىرەخانىغا قوشۇلىشىغا رۇخسەت قىلدى", "user_changed_guest_access_open": "قوللانغۇچى ئەزالارنىڭ مۇنازىرەخانىغا قوشۇلىشىغا رۇخسەت قىلدى",
"user_changed_guest_access_closed": "قوللانغۇچى ئەزالارنىڭ مۇنازىرەخانىغا قوشۇلۇشتىن رەت قىلىندى", "user_changed_guest_access_closed": "قوللانغۇچى ئەزالارنىڭ مۇنازىرەخانىغا قوشۇلۇشتىن رەت قىلىندى",
"user_powerlevel_change_from_to": "قوللانغۇچى بۇرۇنقى دەرىجىسىدىن يېڭى دەرىجىسىگە كۆتۈرىلدى", "user_powerlevel_change_from_to": "قوللانغۇچى بۇرۇنقى دەرىجىسىدىن يېڭى دەرىجىسىگە كۆتۈرىلدى",
"scale_image": "كىچىكلەنمە رەسىم", "scale_image": "كىچىكلەنمە رەسىم",
"users_are_typing": "ئەزالىرى يېزىۋاتىدۇ {نەپەر}", "users_are_typing": "ئەزالىرى يېزىۋاتىدۇ {count}",
"user_is_typing": "قوللانغۇچى يېزىۋاتىدۇ", "user_is_typing": "قوللانغۇچى يېزىۋاتىدۇ",
"your_message": "ئۇچۇرىڭىز ...", "your_message": "ئۇچۇرىڭىز ...",
"replying_to": "{user}", "replying_to": "{user}",
@ -64,19 +59,13 @@
"room_history_invited": "ئەزالار تەكلىپ قىلىنغان ۋاقىتتىن باشلاپ ئوقۇغىلى بولىدۇ", "room_history_invited": "ئەزالار تەكلىپ قىلىنغان ۋاقىتتىن باشلاپ ئوقۇغىلى بولىدۇ",
"room_history_shared": "مۇنازىرەخانىدىكى ھەركىم ئوقۇيالايدۇ", "room_history_shared": "مۇنازىرەخانىدىكى ھەركىم ئوقۇيالايدۇ",
"room_history_world_readable": "ھەركىم ئوقۇيالايدۇ", "room_history_world_readable": "ھەركىم ئوقۇيالايدۇ",
"user_changed_room_history": "قوللانغۇچى» مۇنازىرەخانىنىڭ تارىخىنى قۇردى»", "user_changed_room_history": "قوللانغۇچى» مۇنازىرەخانىنىڭ تارىخىنى قۇردى»"
"reply_image": "",
"reply_audio_message": "",
"reply_video": "",
"reply_poll": "",
"outgoing_message_deleted_text": "",
"incoming_message_deleted_text": ""
}, },
"language_display_name": "ئۇيغۇرچە", "language_display_name": "ئۇيغۇرچە",
"new_room": { "new_room": {
"link_copied": "ئۇلىنىش كۆچۈرۈلدى!", "link_copied": "ئۇلىنىش كۆچۈرۈلدى!",
"status_avatar": "باش سۈرىتى يۈكلىنىۋاتىدۇ: {ئومۇمىي}", "status_avatar": "باش سۈرىتى يۈكلىنىۋاتىدۇ: {count}",
"status_avatar_total": "باش سۈرىتىنى يۈكلەش: {ئومۇمىي} نىڭ {سان}", "status_avatar_total": "باش سۈرىتىنى يۈكلەش: {count} نىڭ {total}",
"status_creating": "مۇنازىرە ئۆيى قۇرۇڭ", "status_creating": "مۇنازىرە ئۆيى قۇرۇڭ",
"invite_description": "تىزىملىكتىن تاللاڭ ياكى ھېسابات كىملىكى ئارقىلىق ئىزدەڭ", "invite_description": "تىزىملىكتىن تاللاڭ ياكى ھېسابات كىملىكى ئارقىلىق ئىزدەڭ",
"invite_info": "پەقەت ئەزالارنى قوشۇش", "invite_info": "پەقەت ئەزالارنى قوشۇش",
@ -91,9 +80,7 @@
"name_room": "مۇنازىرەخانىغا ئىسىم قويۇڭ", "name_room": "مۇنازىرەخانىغا ئىسىم قويۇڭ",
"next": "كېيىنكى", "next": "كېيىنكى",
"create": "قۇرۇش", "create": "قۇرۇش",
"new_room": "يېڭى مۇنازىرەخانا", "new_room": "يېڭى مۇنازىرەخانا"
"room_name_limit_error_msg": "",
"colon_not_allowed": ""
}, },
"room": { "room": {
"purge_failed": "مۇنازىرەخانىنى يۇيۇش مەغلۇب بولدى!", "purge_failed": "مۇنازىرەخانىنى يۇيۇش مەغلۇب بولدى!",
@ -103,37 +90,30 @@
"purge_redacting_events": "پائالىيەتلەرنى تەھرىرلەش", "purge_redacting_events": "پائالىيەتلەرنى تەھرىرلەش",
"purge_set_room_state": "مۇنازىرەخانىنىڭ شەرتىنى قۇرۇش", "purge_set_room_state": "مۇنازىرەخانىنىڭ شەرتىنى قۇرۇش",
"leave": "كېتىش", "leave": "كېتىش",
"members": "ئەزالار يوق | بىر ئەزا | [نەپەر] ئەزا", "members": "ئەزالار يوق | بىر ئەزا | [نەپەر] ئەزا"
"room_name_required": "",
"room_topic_required": ""
}, },
"leave": { "leave": {
"text_public_lastroom": "ئەگەر بۇ ئۆيگە يەنە قوشۇلماقچى بولسىڭىز ، يېڭى سالاھىيەت ئاستىدا قاتناشسىڭىز بولىدۇ. {ئىشلەتكۈچى} ، {ھەرىكەت} نى ساقلاش.", "text_public_lastroom": "ئەگەر بۇ ئۆيگە يەنە قوشۇلماقچى بولسىڭىز ، يېڭى سالاھىيەت ئاستىدا قاتناشسىڭىز بولىدۇ. {user} ، {action} نى ساقلاش.",
"leave": "كېتىڭ", "leave": "كېتىڭ",
"go_back": "قايتىڭ", "go_back": "قايتىڭ",
"create_account": "ھېسابات قۇرۇڭ", "create_account": "ھېسابات قۇرۇڭ",
"text_invite": "بۇ ئۆي قۇلۇپلانغان. ئالاھىدە ئىجازەت ئالماي تۇرۇپ قايتا جەم بولالمايسىز.", "text_invite": "بۇ ئۆي قۇلۇپلانغان. ئالاھىدە ئىجازەت ئالماي تۇرۇپ قايتا جەم بولالمايسىز.",
"title_invite": "كەتمەكچىمۇ؟", "title_invite": "كەتمەكچىمۇ؟",
"text_public": "ئۇلىنىشنى بىلسىڭىز ھەمىشە بۇ ئۆيگە قايتا كىرەلەيسىز.", "text_public": "ئۇلىنىشنى بىلسىڭىز ھەمىشە بۇ ئۆيگە قايتا كىرەلەيسىز.",
"title_public": "خەير خوش ، {ئىشلەتكۈچى}" "title_public": "خەير خوش ، {user}"
}, },
"logout": { "logout": {
"confirm_text": ""
}, },
"join": { "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": "مۇنازىرە ئۆيىگە قوشۇلۇش مەغلۇب بولدى.", "join_failed": "مۇنازىرە ئۆيىگە قوشۇلۇش مەغلۇب بولدى.",
"choose_name": "" "status_joining": "مۇنازىرىگە كىرىش...",
"status_logging_in": "كىرىش ...",
"enter_room": "مۇنازىرىگە قوشۇلۇڭ",
"join": "مۇنازىرىگە قوشۇلۇڭ",
"joining_as": "سىز تۆۋەندىكىدەك قاتنىشىۋاتىسىز:",
"user_name_label": "قوللانغۇچى ئىسمى",
"title": "{ياتاق ئىسمى} غا خۇش كەپسىز",
"user_name_label": "قوللانغۇچى ئىسمى"
}, },
"room_welcome": { "room_welcome": {
"info_permissions": "ياتاق تەڭشىكىدە خالىغان ۋاقىتتا «قوشۇلۇش ئىجازەتنامىسى» نى ئۆزگەرتەلەيسىز.", "info_permissions": "ياتاق تەڭشىكىدە خالىغان ۋاقىتتا «قوشۇلۇش ئىجازەتنامىسى» نى ئۆزگەرتەلەيسىز.",
@ -141,7 +121,7 @@
"join_invite": "سىز تەكلىپ قىلغان كىشىلەرلا قاتناشسا بولىدۇ.", "join_invite": "سىز تەكلىپ قىلغان كىشىلەرلا قاتناشسا بولىدۇ.",
"join_public": "ھەركىم بۇ ئۇلىنىشنى ئېچىش ئارقىلىق قوشۇلالايدۇ: {ئۇلىنىش}.", "join_public": "ھەركىم بۇ ئۇلىنىشنى ئېچىش ئارقىلىق قوشۇلالايدۇ: {ئۇلىنىش}.",
"room_history_joined": "ئەزالارقوشۇلغاندىن كېيىنلا ئەۋەتىلگەن ئۇچۇرلارنى كۆرەلەيدۇ.", "room_history_joined": "ئەزالارقوشۇلغاندىن كېيىنلا ئەۋەتىلگەن ئۇچۇرلارنى كۆرەلەيدۇ.",
"room_history_is": "مۇنازىرەخانا تارىخى {تىپى}.", "room_history_is": "مۇنازىرەخانا تارىخى {type}.",
"encrypted": "ئۇچۇرلار ئاخىرىغىچە مەخپىيلەشتۈرۈلگەن.", "encrypted": "ئۇچۇرلار ئاخىرىغىچە مەخپىيلەشتۈرۈلگەن.",
"info": "خۇش كەپسىز! مۇنازىرەخانا ھەققىدە بىلىشكە تېگىشلىك بىر قانچە ئىش:" "info": "خۇش كەپسىز! مۇنازىرەخانا ھەققىدە بىلىشكە تېگىشلىك بىر قانچە ئىش:"
}, },
@ -171,7 +151,7 @@
}, },
"room_info": { "room_info": {
"scan_code": "سىكانېرلاپ ئۆيگە قوشۇلۇڭ", "scan_code": "سىكانېرلاپ ئۆيگە قوشۇلۇڭ",
"version_info": "قوغدىغۇچى تۈرى تەرىپىدىن ئىشلەنگەن. نەشرى: {نەشرى}", "version_info": "قوغدىغۇچى تۈرى تەرىپىدىن ئىشلەنگەن. نەشرى: {version}",
"leave_room": "ئايرىلماق", "leave_room": "ئايرىلماق",
"show_all": "<ھەممىنى كۆرسەتمەك", "show_all": "<ھەممىنى كۆرسەتمەك",
"hide_all": "يوشۇرۇن", "hide_all": "يوشۇرۇن",
@ -193,30 +173,30 @@
"room_deleted": "ئۆي ئۆچۈرۈلدى." "room_deleted": "ئۆي ئۆچۈرۈلدى."
}, },
"purge_room": { "purge_room": {
"room_deletion_notice": "خوشلىشىش ۋاقتى! بۇ ئۆي {ئىشلەتكۈچى} تەرىپىدىن ئۆچۈرۈلدى. ئۇ سېكۇنتتا ئۆزىنى ھالاك قىلىدۇ.", "room_deletion_notice": "خوشلىشىش ۋاقتى! بۇ ئۆي {user} تەرىپىدىن ئۆچۈرۈلدى. ئۇ سېكۇنتتا ئۆزىنى ھالاك قىلىدۇ.",
"notified": "ئەزالارغا ئۇقتۇرۇش قىلدۇق.", "notified": "ئەزالارغا ئۇقتۇرۇش قىلدۇق.",
"deleting": "ئۆچۈرۈش ئۆيى:", "deleting": "ئۆچۈرۈش ئۆيى:",
"self_destruct": "ئۆي سېكۇنتتا ئۆزىنى ھالاك قىلىدۇ.", "self_destruct": "ئۆي سېكۇنتتا ئۆزىنى ھالاك قىلىدۇ.",
"n_seconds": "{سېكۇنت} سېكۇنت", "n_seconds": "{seconds} سېكۇنت",
"button": "ئۆچۈرۈش", "button": "ئۆچۈرۈش",
"info": "بارلىق ئەزالار ۋە ئۇچۇرلار ئۆچۈرۈلىدۇ. بۇ ھەرىكەتنى ئەمەلدىن قالدۇرغىلى بولمايدۇ.", "info": "بارلىق ئەزالار ۋە ئۇچۇرلار ئۆچۈرۈلىدۇ. بۇ ھەرىكەتنى ئەمەلدىن قالدۇرغىلى بولمايدۇ.",
"title": "ئۆينى ئۆچۈرەمسىز؟" "title": "ئۆينى ئۆچۈرەمسىز؟"
}, },
"invite": { "invite": {
"status_error": "بىر ياكى بىر نەچچە دوستنى تەكلىپ قىلىش مەغلۇب بولدى!", "status_error": "بىر ياكى بىر نەچچە دوستنى تەكلىپ قىلىش مەغلۇب بولدى!",
"status_inviting": "{سان} نىڭ دوستى {كۆرسەتكۈچ} نى تەكلىپ قىلىش", "status_inviting": "{count} نىڭ دوستى {index} نى تەكلىپ قىلىش",
"send_invites_to": "تەكلىپ قىلىش", "send_invites_to": "تەكلىپ قىلىش",
"done": "تاماملاندى", "done": "تاماملاندى",
"title": "دوست قوشۇڭ" "title": "دوست قوشۇڭ"
}, },
"profile_info_popup": { "profile_info_popup": {
"new_room": "+ يېڭى ئۆي", "new_room": "+ يېڭى ئۆي",
"powered_by": "بۇ مۇنازىرە ئۆيى {مەھسۇلات} ئىشلىتىلگەن.xx دىن تېخىمۇ كۆپ بىلىمگە ئېرىشىڭ ياكى ئىلگىرىلەپ باشقا ئۆي قۇرۇڭ!", "powered_by": "بۇ مۇنازىرە ئۆيى {product} ئىشلىتىلگەن.xx دىن تېخىمۇ كۆپ بىلىمگە ئېرىشىڭ ياكى ئىلگىرىلەپ باشقا ئۆي قۇرۇڭ!",
"want_more": "تېخىمۇ كۆپ خالامسىز؟", "want_more": "تېخىمۇ كۆپ خالامسىز؟",
"logout": "چېكىنىش", "logout": "چېكىنىش",
"edit_profile": "ئارخىپنى تەھرىرلەش", "edit_profile": "ئارخىپنى تەھرىرلەش",
"identity_temporary": "{كۆرسىتىش ئىسمى}", "identity_temporary": "{displayName}",
"identity": "{كۆرسىتىش ئىسمى}", "identity": "{displayName}",
"you_are": "سىز" "you_are": "سىز"
}, },
"profile": { "profile": {
@ -225,16 +205,11 @@
"password_new": "يېڭى پارول", "password_new": "يېڭى پارول",
"password_old": "كونا پارول", "password_old": "كونا پارول",
"select_language": "تىل", "select_language": "تىل",
"set_language": "",
"language_description": "",
"dont_see_yours": "",
"tell_us": "",
"change_password": "پارولنى ئۆزگەرتىڭ", "change_password": "پارولنى ئۆزگەرتىڭ",
"change_name": "ئىسىم ئۆزگەرتىش", "change_name": "ئىسىم ئۆزگەرتىش",
"set_password": "پارول بەلگىلەڭ", "set_password": "پارول بەلگىلەڭ",
"temporary_identity": "بۇ كىملىك ۋاقىتلىق. قايتا ئىشلىتىش ئۈچۈن پارول بەلگىلەڭ", "temporary_identity": "بۇ كىملىك ۋاقىتلىق. قايتا ئىشلىتىش ئۈچۈن پارول بەلگىلەڭ",
"title": "مېنىڭ ئارخىپىم", "title": "مېنىڭ ئارخىپىم"
"display_name_required": ""
}, },
"login": { "login": {
"login": "‎كىرىش", "login": "‎كىرىش",
@ -242,10 +217,7 @@
"username_required": "قوللانغۇچى ئىسمى تەلەپ قىلىنىدۇ", "username_required": "قوللانغۇچى ئىسمى تەلەپ قىلىنىدۇ",
"password": "پارول كىرگۈزۈڭ", "password": "پارول كىرگۈزۈڭ",
"username": "قوللانغۇچى ئىسمى (مەسىلەن: marta)", "username": "قوللانغۇچى ئىسمى (مەسىلەن: marta)",
"title": "كىرىش", "title": "كىرىش"
"create_room": "",
"or": "",
"invalid_message": ""
}, },
"device_list": { "device_list": {
"not_verified": "دەلىللەنمىدى", "not_verified": "دەلىللەنمىدى",

View file

@ -3,11 +3,6 @@
"name": "Convene", "name": "Convene",
"tag_line": "只需连接" "tag_line": "只需连接"
}, },
"global": {
"save": "",
"password_didnot_match": "",
"password_hint": ""
},
"fallbacks": { "fallbacks": {
"download_name": "下载", "download_name": "下载",
"original_text": "<原文>", "original_text": "<原文>",
@ -35,7 +30,16 @@
"permissions": "加入权限", "permissions": "加入权限",
"created_by": "由 {user} 创建", "created_by": "由 {user} 创建",
"copy_link": "复制邀请链接", "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": {
"leave": "离开", "leave": "离开",
@ -47,9 +51,6 @@
"text_public": "如果您知道链接,您可以随时再次加入此聊天室。", "text_public": "如果您知道链接,您可以随时再次加入此聊天室。",
"title_public": "再见,{user}" "title_public": "再见,{user}"
}, },
"logout": {
"confirm_text": ""
},
"login": { "login": {
"login": "登录", "login": "登录",
"password": "输入密码", "password": "输入密码",
@ -59,7 +60,15 @@
"username": "用户名 (如: marta)", "username": "用户名 (如: marta)",
"create_room": "注册并创建聊天室", "create_room": "注册并创建聊天室",
"or": "或者", "or": "或者",
"invalid_message": "用户名或密码无效" "invalid_message": "用户名或密码无效",
"resend_verification": "重新发送验证邮件",
"sent_verification": "一封电子邮件已发送至 {email}。 请使用您的常用电子邮件客户端来验证地址。",
"no_supported_flow": "该应用程序无法登录到给出的服务器",
"send_verification": "发送验证邮件",
"email_not_valid": "电子邮件地址无效",
"terms": "主服务器要求您查看并接受以下政策:",
"accept_terms": "接受",
"email": "您需要验证您的电子邮件地址"
}, },
"device_list": { "device_list": {
"title": "设备", "title": "设备",
@ -73,11 +82,14 @@
"room_list_rooms": "聊天室", "room_list_rooms": "聊天室",
"room_list_invites": "邀请", "room_list_invites": "邀请",
"purge_failed": "删除聊天室失败了!", "purge_failed": "删除聊天室失败了!",
"purge_removing_members": "移除成员", "purge_removing_members": "移除成员{count} 个,共 {total} 个)",
"purge_redacting_events": "编辑事件", "purge_redacting_events": "编辑事件{count} 个,共 {total} 个)",
"purge_set_room_state": "设置聊天室状态", "purge_set_room_state": "设置聊天室状态",
"room_name_required": "", "invitations": "您没有邀请 | 您有 1 个邀请 | 您有 {count} 个邀请",
"room_topic_required": "" "unseen_messages": "你没有任何未读信息 | 您有 1 条未读信息 | 您有 {count} 条未读信息",
"room_list_new_messages": "{count} 条新消息",
"room_name_required": "聊天室名称必填",
"room_topic_required": "需要聊天室描述"
}, },
"message": { "message": {
"you": "您", "you": "您",
@ -116,12 +128,21 @@
"replying_to": "{user}", "replying_to": "{user}",
"user_changed_guest_access_open": "{user} 允许客人加入聊天室", "user_changed_guest_access_open": "{user} 允许客人加入聊天室",
"user_changed_guest_access_closed": "{user} 不允许客人加入聊天室", "user_changed_guest_access_closed": "{user} 不允许客人加入聊天室",
"reply_image": "", "reply_audio_message": "语音留言",
"reply_audio_message": "", "reply_video": "视频",
"reply_video": "", "reply_image": "图片",
"reply_poll": "", "user_was_kicked": "{user} 被移除了聊天。",
"outgoing_message_deleted_text": "", "user_was_kicked_by_you": "您把{user} 移除了聊天室。",
"incoming_message_deleted_text": "" "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": { "menu": {
"login": "登录", "login": "登录",
@ -140,7 +161,12 @@
"loading": "正在加载 {appName}", "loading": "正在加载 {appName}",
"ignore": "忽略", "ignore": "忽略",
"join": "加入", "join": "加入",
"undo": "撤销" "undo": "撤销",
"user_kick_and_ban": "移出并禁止该用户",
"user_kick": "移出该用户",
"user_make_admin": "设为管理员",
"user_make_moderator": "设为版主",
"user_revoke_moderator": "撤销版主身份"
}, },
"power_level": { "power_level": {
"restricted": "被限制", "restricted": "被限制",
@ -180,18 +206,18 @@
}, },
"join": { "join": {
"title": "欢迎您被邀请加入", "title": "欢迎您被邀请加入",
"title_user": "",
"user_name_label": "用户名", "user_name_label": "用户名",
"remember_me": "记得我", "remember_me": "记得我",
"joining_as": "您以以下身份加入:", "joining_as": "您以以下身份加入:",
"join": "加入聊天室", "join": "加入聊天室",
"join_user": "",
"enter_room": "加入聊天室", "enter_room": "加入聊天室",
"enter_room_user": "",
"status_logging_in": "正在登录中...", "status_logging_in": "正在登录中...",
"status_joining": "正在加入聊天室...", "status_joining": "正在加入聊天室...",
"join_failed": "加入聊天室失败。", "join_failed": "加入聊天室失败。",
"choose_name": "" "title_user": "欢迎您被邀请聊天",
"join_user": "开始聊天",
"enter_room_user": "开始聊天",
"choose_name": "选择要使用的名称"
}, },
"profile": { "profile": {
"display_name": "显示名称", "display_name": "显示名称",
@ -208,7 +234,7 @@
"language_description": "Convene 提供多种语言.", "language_description": "Convene 提供多种语言.",
"dont_see_yours": "看不到你的?", "dont_see_yours": "看不到你的?",
"tell_us": "告诉我们。", "tell_us": "告诉我们。",
"display_name_required": "" "display_name_required": "显示名称是必需的"
}, },
"new_room": { "new_room": {
"status_avatar": "正在上传头像:{count}", "status_avatar": "正在上传头像:{count}",
@ -224,14 +250,15 @@
"join_permissions_info": "这些权限决定了人们如何加入聊天室以及邀请其他人的难易程度。 你可以随时更改它们。", "join_permissions_info": "这些权限决定了人们如何加入聊天室以及邀请其他人的难易程度。 你可以随时更改它们。",
"set_join_permissions": "设置加入权限", "set_join_permissions": "设置加入权限",
"join_permissions": "加入权限", "join_permissions": "加入权限",
"name_room": "命名聊天室", "name_room": "聊天室名称",
"next": "下一步", "next": "下一步",
"done": "完毕", "done": "完毕",
"new_room": "新的聊天室", "new_room": "新的聊天室",
"room_topic": "如果您愿意,请添加说明", "room_topic": "如果您愿意,请添加说明",
"create": "创建", "create": "创建",
"room_name_limit_error_msg": "", "colon_not_allowed": "冒号是不允许的",
"colon_not_allowed": "" "options": "选项",
"room_name_limit_error_msg": "最多允许 50 个字符"
}, },
"room_welcome": { "room_welcome": {
"got_it": "知道了", "got_it": "知道了",
@ -245,7 +272,7 @@
"encrypted": "信息是端到端加密的。" "encrypted": "信息是端到端加密的。"
}, },
"profile_info_popup": { "profile_info_popup": {
"new_room": "+ 新的聊天室", "new_room": "新的聊天室",
"powered_by": "这个聊天室由 {product} 提供支持。 在 {productLink} 了解更多信息或继续创建另一个聊天室!", "powered_by": "这个聊天室由 {product} 提供支持。 在 {productLink} 了解更多信息或继续创建另一个聊天室!",
"want_more": "想要更多?", "want_more": "想要更多?",
"logout": "退出登录", "logout": "退出登录",
@ -259,5 +286,51 @@
"close_tab": "关闭浏览器标签", "close_tab": "关闭浏览器标签",
"room_deleted": "聊天室已删除。" "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}"
}
} }

View file

@ -6,7 +6,7 @@
v-on="$listeners" v-on="$listeners"
> >
<v-col cols="auto" class="me-2"> <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>
<v-col>{{ text }}</v-col> <v-col>{{ text }}</v-col>
</v-row> </v-row>
@ -22,6 +22,12 @@ export default {
return null; return null;
}, },
}, },
iconSize: {
type: Number,
default: function() {
return 22;
}
},
text: { text: {
type: String, type: String,
default: function () { default: function () {

View file

@ -56,12 +56,14 @@
</div> </div>
<div class="load-later"> <div class="load-later">
<v-btn v-if="canRecordAudio" class="mic-button" ref="mic_button" fab small elevation="0" v-blur <v-btn :class="{'mic-button': true, 'dimmed': !canRecordAudio}" ref="mic_button" fab small elevation="0" v-blur
@click.stop="$emit('start-recording')"> @click.stop="micButtonClicked()">
<v-icon color="white">mic</v-icon> <v-icon color="white">mic</v-icon>
</v-btn> </v-btn>
<v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon> <v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon>
</div> </div>
<div v-if="showReadOnlyToast" class="toast-read-only">{{ $t("message.not_allowed_to_send") }}</div>
</div> </div>
</template> </template>
@ -111,6 +113,7 @@ export default {
playing: false, playing: false,
analyzer: null, analyzer: null,
analyzerDataArray: null, analyzerDataArray: null,
showReadOnlyToast: false,
}; };
}, },
mounted() { mounted() {
@ -162,12 +165,7 @@ export default {
}, },
computed: { computed: {
canRecordAudio() { canRecordAudio() {
if (this.room) { return !this.$matrix.currentRoomIsReadOnlyForUser && util.browserCanRecordAudio();
const myUserId = this.$matrix.currentUserId;
const me = this.room.getMember(myUserId);
return me && me.powerLevelNorm > 0 && util.browserCanRecordAudio();
}
return false;
}, },
currentTime() { currentTime() {
return util.formatDuration(this.playTime); return util.formatDuration(this.playTime);
@ -448,6 +446,17 @@ export default {
} }
return null; return null;
}, },
micButtonClicked() {
if (this.$matrix.currentRoomIsReadOnlyForUser) {
this.showReadOnlyToast = true;
setTimeout(() => {
this.showReadOnlyToast = false;
}, 3000);
} else {
this.$emit('start-recording');
}
}
} }
}; };
</script> </script>

View file

@ -24,6 +24,7 @@
color="black" color="black"
@click.stop="onBackgroundClick" @click.stop="onBackgroundClick"
class="bottom-sheet-close" class="bottom-sheet-close"
v-if="showCloseButton"
> >
<v-icon color="white" >cancel</v-icon> <v-icon color="white" >cancel</v-icon>
</v-btn> </v-btn>
@ -40,6 +41,10 @@ import Hammer from "hammerjs";
export default { export default {
props: { props: {
showCloseButton: {
type: Boolean,
default: true,
},
openY: { openY: {
type: Number, type: Number,
default: 0.1, default: 0.1,

View file

@ -1,15 +1,12 @@
<template> <template>
<div class="chat-root fill-height d-flex flex-column"> <div class="chat-root fill-height d-flex flex-column">
<div class="chat-room-invitations clickable" v-if="invitationCount > 0" @click.stop="onInvitationsClick"> <ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" v-on:view-room-details="viewRoomDetails" />
{{ $tc("room.invitations", invitationCount) }}
</div>
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" />
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room" <AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room"
:events="events" :autoplay="!showRecorder" :events="events" :autoplay="!showRecorder"
:timelineSet="timelineSet" :timelineSet="timelineSet"
:readMarker="readMarker" :readMarker="readMarker"
:recordingMembers="typingMembers" :recordingMembers="typingMembers"
v-on:start-recording="showRecorder = true" v-on:start-recording="setShowRecorder()"
v-on:loadnext="handleScrolledToBottom(false)" v-on:loadnext="handleScrolledToBottom(false)"
v-on:loadprevious="handleScrolledToTop()" v-on:loadprevious="handleScrolledToTop()"
v-on:mark-read="sendRR" v-on:mark-read="sendRR"
@ -45,9 +42,9 @@
<CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" /> <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 --> <!-- 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 v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
<div class="message-wrapper" v-on:touchstart=" <div class="message-wrapper" v-on:touchstart="
@ -55,18 +52,25 @@
touchStart(e, event); touchStart(e, event);
} }
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove"> " v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
<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" :timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
v-on:context-menu="showContextMenuForEvent($event)" v-on:own-avatar-clicked="viewProfile" 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: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">EventID: {{ event.getId() }}</div> -->
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> --> <!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
<div v-if="event.getId() == readMarker && index < events.length - 1" class="read-marker" <div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"
:title="$t('message.unread_messages')" /> :title="$t('message.unread_messages')" />
</div> </div>
</div> </div>
</div> </div>
<NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />
</div> </div>
<!-- Input area --> <!-- Input area -->
@ -109,7 +113,7 @@
{{ typingMembersString }} {{ typingMembersString }}
</div> </div>
</v-row> </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-col class="flex-grow-1 flex-shrink-1 ma-0 pa-0">
<v-textarea height="undefined" ref="messageInput" full-width auto-grow rows="1" v-model="currentInput" <v-textarea height="undefined" ref="messageInput" full-width auto-grow rows="1" v-model="currentInput"
no-resize class="input-area-text" :placeholder="$t('message.your_message')" hide-details no-resize class="input-area-text" :placeholder="$t('message.your_message')" hide-details
@ -126,7 +130,7 @@
</v-btn> </v-btn>
</v-col> </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"> class="input-area-button text-center flex-grow-0 flex-shrink-1">
<v-btn icon large color="black" @click="showCreatePollDialog = true"> <v-btn icon large color="black" @click="showCreatePollDialog = true">
<v-icon dark>$vuetify.icons.poll</v-icon> <v-icon dark>$vuetify.icons.poll</v-icon>
@ -162,7 +166,7 @@
</v-col> </v-col>
<v-col v-if="$config.shortCodeStickers" class="input-area-button text-center flex-grow-0 flex-shrink-1"> <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"> :disabled="attachButtonDisabled">
<v-icon large>face</v-icon> <v-icon large>face</v-icon>
</v-btn> </v-btn>
@ -170,7 +174,7 @@
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1"> <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1">
<label icon flat ref="attachmentLabel"> <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"> :disabled="attachButtonDisabled">
<v-icon x-large>add_circle_outline</v-icon> <v-icon x-large>add_circle_outline</v-icon>
</v-btn> </v-btn>
@ -180,10 +184,11 @@
<VoiceRecorder :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder" <VoiceRecorder :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" /> v-on:close="showRecorder = false" v-on:file="onVoiceRecording" />
</div> </div>
<div v-if="!useVoiceMode && room && $matrix.currentRoomIsReadOnlyForUser" class="input-area-read-only">{{ $t("message.not_allowed_to_send") }}</div>
</v-container> </v-container>
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)" <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"> <div v-if="currentImageInputPath">
<v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'"> <v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'">
@ -257,7 +262,7 @@
<script> <script>
import Vue from "vue"; 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 util from "../plugins/utils";
import MessageOperations from "./messages/MessageOperations.vue"; import MessageOperations from "./messages/MessageOperations.vue";
import AvatarOperations from "./messages/AvatarOperations.vue"; import AvatarOperations from "./messages/AvatarOperations.vue";
@ -265,6 +270,7 @@ import ChatHeader from "./ChatHeader";
import VoiceRecorder from "./VoiceRecorder"; import VoiceRecorder from "./VoiceRecorder";
import RoomInfoBottomSheet from "./RoomInfoBottomSheet"; import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader"; import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet"; import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet"; import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
import BottomSheet from "./BottomSheet.vue"; import BottomSheet from "./BottomSheet.vue";
@ -314,6 +320,7 @@ export default {
VoiceRecorder, VoiceRecorder,
RoomInfoBottomSheet, RoomInfoBottomSheet,
CreatedRoomWelcomeHeader, CreatedRoomWelcomeHeader,
NoHistoryRoomWelcomeHeader,
MessageOperationsBottomSheet, MessageOperationsBottomSheet,
StickerPickerBottomSheet, StickerPickerBottomSheet,
BottomSheet, BottomSheet,
@ -324,6 +331,7 @@ export default {
data() { data() {
return { return {
waitingForRoomObject: false,
events: [], events: [],
currentInput: "", currentInput: "",
typingMembers: [], typingMembers: [],
@ -421,7 +429,6 @@ export default {
computed: { computed: {
chatContainer() { chatContainer() {
const container = this.$refs.chatContainer; const container = this.$refs.chatContainer;
console.log("GOT CONTAINER", container);
if (this.useVoiceMode) { if (this.useVoiceMode) {
return container.$el; return container.$el;
} }
@ -461,7 +468,7 @@ export default {
return this.fullyReadMarker || this.room.getEventReadUpTo(this.$matrix.currentUserId, false); return this.fullyReadMarker || this.room.getEventReadUpTo(this.$matrix.currentUserId, false);
}, },
fullyReadMarker() { fullyReadMarker() {
const readEvent = this.room.getAccountData("m.fully_read"); const readEvent = this.room && this.room.getAccountData("m.fully_read");
if (readEvent) { if (readEvent) {
return readEvent.getContent().event_id; return readEvent.getContent().event_id;
} }
@ -513,9 +520,6 @@ export default {
debugging() { debugging() {
return false; //(window.location.host || "").startsWith("localhost"); return false; //(window.location.host || "").startsWith("localhost");
}, },
invitationCount() {
return this.$matrix.invites.length;
},
canCreatePoll() { canCreatePoll() {
// We say that if you can redact events, you are allowed to create polls. // We say that if you can redact events, you are allowed to create polls.
const me = this.room && this.room.getMember(this.$matrix.currentUserId); const me = this.room && this.room.getMember(this.$matrix.currentUserId);
@ -529,6 +533,34 @@ export default {
return util.useVoiceMode(this.room); 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: { watch: {
@ -552,6 +584,7 @@ export default {
this.$matrix.off("Room.timeline", this.onEvent); this.$matrix.off("Room.timeline", this.onEvent);
this.$matrix.off("RoomMember.typing", this.onUserTyping); this.$matrix.off("RoomMember.typing", this.onUserTyping);
this.waitingForRoomObject = false;
this.events = []; this.events = [];
this.timelineWindow = null; this.timelineWindow = null;
this.typingMembers = []; this.typingMembers = [];
@ -562,27 +595,32 @@ export default {
this.stopRRTimer(); this.stopRRTimer();
this.lastRR = null; this.lastRR = null;
if (!this.room) { if (this.roomId) {
// Public room? this.$matrix.isJoinedToRoom(this.roomId).then(joined => {
if (this.roomId && this.roomId.startsWith("#")) { if (!joined) {
this.onRoomNotJoined(); this.onRoomNotJoined();
} else if (this.roomId) { } else {
this.onRoomNotJoined(); // Private room we are not joined to. What to do? We redirect to join if (this.room) {
// screen, maybe the user has an invite already? 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; this.initialLoadDone = true;
return; // no room 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() { showMessageOperations() {
if (this.showMessageOperations) { if (this.showMessageOperations) {
this.$nextTick(() => { this.$nextTick(() => {
@ -597,7 +635,7 @@ export default {
var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect(); var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect(); var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
top = rectAnchor.top - rectChat.top - 50; top = rectAnchor.top - rectChat.top - 50;
left = rectAnchor.left - rectChat.left - 50; left = rectAnchor.left - rectChat.left - 75;
if (left + rectOps.width >= rectChat.right) { if (left + rectOps.width >= rectChat.right) {
left = rectChat.right - rectOps.width - 10; // No overflow left = rectChat.right - rectOps.width - 10; // No overflow
} }
@ -898,7 +936,6 @@ export default {
var reader = new FileReader(); var reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const file = event.target.files[0]; const file = event.target.files[0];
this.currentSendShowSendButton = true;
if (file.type.startsWith("image/")) { if (file.type.startsWith("image/")) {
this.currentImageInput = { this.currentImageInput = {
image: e.target.result, image: e.target.result,
@ -947,7 +984,15 @@ export default {
} }
} }
console.log(this.currentImageInput); 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]); reader.readAsDataURL(event.target.files[0]);
} }
@ -996,7 +1041,7 @@ export default {
} }
}) })
.catch((err) => { .catch((err) => {
if (err instanceof AbortError || err === "Abort") { if (err.name === "AbortError" || err === "Abort") {
this.currentSendError = null; this.currentSendError = null;
} else { } else {
this.currentSendError = err.LocaleString(); this.currentSendError = err.LocaleString();
@ -1436,16 +1481,17 @@ export default {
}, },
onHeaderClick() { onHeaderClick() {
const invitations = this.$matrix.invites.length;
const joinedRooms = this.$matrix.joinedRooms; 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! // Only joined to this room, go directly to room details!
this.$navigation.push({ name: "RoomInfo" }); this.$navigation.push({ name: "RoomInfo" });
return; return;
} }
this.$refs.roomInfoSheet.open(); this.$refs.roomInfoSheet.open();
}, },
onInvitationsClick() { viewRoomDetails() {
this.$navigation.push({ name: "Home" }, -1); this.$navigation.push({ name: "RoomInfo" });
}, },
pollWasClosed(ignoredE) { pollWasClosed(ignoredE) {
let div = document.createElement("div"); let div = document.createElement("div");
@ -1455,7 +1501,15 @@ export default {
setTimeout(() => { setTimeout(() => {
this.chatContainer.parentElement.removeChild(div); this.chatContainer.parentElement.removeChild(div);
}, 3000); }, 3000);
},
setShowRecorder() {
if (this.canRecordAudio) {
this.showRecorder = true;
} else {
this.showNoRecordingAvailableDialog = true;
}
} }
}, },
}; };
</script> </script>

View file

@ -4,89 +4,81 @@
<v-col <v-col
cols="auto" cols="auto"
class="chat-header-members text-start ma-0 pa-0" class="chat-header-members text-start ma-0 pa-0"
@click.stop="onHeaderClicked"
> >
<v-avatar size="40" class="me-2"> <v-avatar size="40" class="clickable me-2 chat-header-avatar" color="grey" @click.stop="onAvatarClicked">
<v-img v-if="room.avatar || memberAvatar" :src="room.avatar || memberAvatar" /> <v-img v-if="roomAvatar" :src="roomAvatar" />
<span v-else class="white--text headline">{{
room.name.substring(0, 1).toUpperCase()
}}</span>
</v-avatar> </v-avatar>
</v-col> </v-col>
<v-col class="chat-header-name ma-0 pa-0 flex-shrink-1 flex-grow-1 flex-nowrap" @click.stop="onHeaderClicked">
<v-col <div class="room-title-row">
class="chat-header-name ma-0 pa-0 flex-shrink-1 flex-nowrap" <div class="room-name-inline text-truncate" :title="room.name">
@click.stop="onHeaderClicked" {{ room.name }}
> </div>
<div class="room-name-inline text-truncate" :title="room.name"> <v-icon class="icon-dropdown" size="11">$vuetify.icons.ic_dropdown</v-icon>
{{ room.name }}<v-icon class="icon-dropdown" size="11">$vuetify.icons.ic_dropdown</v-icon><div class="notification-alert" v-if="notifications"></div> <div :class="{ 'notification-alert': true, 'popup-open': showMissedItemsInfo }" v-if="notifications">
<!-- MISSED ITEMS POPUP -->
<!--<v-icon>expand_more</v-icon>--> <!-- <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>
<div class="num-members">{{ $tc("room.members", memberCount) }}</div> <div class="num-members">{{ $tc("room.members", memberCount) }}</div>
</v-col> </v-col>
<v-col cols="auto" class="text-end ma-0 pa-0"> <v-col cols="auto" class="text-end ma-0 pa-0 ms-1">
<v-btn <v-avatar :class="{ 'avatar-32': true, 'clickable': true, 'popup-open': showProfileInfo }" size="26"
id="btn-purge-room" color="#e0e0e0" @click.stop="showProfileInfo = true">
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"
>
<img v-if="userAvatar" :src="userAvatar" /> <img v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
</v-col> </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> </v-row>
<!-- "REALLY LEAVE?" dialog --> <!-- "REALLY LEAVE?" dialog -->
<LeaveRoomDialog <LeaveRoomDialog :show="showLeaveConfirmation" :room="room" @close="showLeaveConfirmation = false" />
:show="showLeaveConfirmation"
:room="room"
@close="showLeaveConfirmation = false"
/>
<!-- PROFILE INFO POPUP --> <!-- PROFILE INFO POPUP -->
<ProfileInfoPopup <ProfileInfoPopup :show="showProfileInfo" @close="showProfileInfo = false" />
:show="showProfileInfo"
@close="showProfileInfo = false" <!-- MORE MENU POPUP -->
/> <MoreMenuPopup :show="showMoreMenu" :menuItems="moreMenuItems" @close="showMoreMenu = false"
v-on:leave="showLeaveConfirmation = true" />
<!-- PURGE ROOM POPUP --> <!-- PURGE ROOM POPUP -->
<PurgeRoomDialog <PurgeRoomDialog :show="showPurgeConfirmation" :room="room" @close="showPurgeConfirmation = false" />
:show="showPurgeConfirmation"
:room="room" <RoomExport :room="room" v-if="downloadingChat" v-on:close="downloadingChat = false" />
@close="showPurgeConfirmation = false"
/>
</v-container> </v-container>
</template> </template>
<script> <script>
import LeaveRoomDialog from "../components/LeaveRoomDialog"; import LeaveRoomDialog from "../components/LeaveRoomDialog";
import ProfileInfoPopup from "../components/ProfileInfoPopup"; import ProfileInfoPopup from "../components/ProfileInfoPopup";
import MoreMenuPopup from "../components/MoreMenuPopup";
import profileInfoMixin from "../components/profileInfoMixin"; import profileInfoMixin from "../components/profileInfoMixin";
import PurgeRoomDialog from "../components/PurgeRoomDialog"; import PurgeRoomDialog from "../components/PurgeRoomDialog";
import RoomExport from "../components/RoomExport";
import roomInfoMixin from "./roomInfoMixin"; import roomInfoMixin from "./roomInfoMixin";
@ -96,14 +88,22 @@ export default {
components: { components: {
LeaveRoomDialog, LeaveRoomDialog,
ProfileInfoPopup, ProfileInfoPopup,
PurgeRoomDialog MoreMenuPopup,
PurgeRoomDialog,
RoomExport
}, },
data() { data() {
return { return {
memberCount: null, memberCount: null,
showLeaveConfirmation: false, showLeaveConfirmation: false,
showProfileInfo: false, showProfileInfo: false,
showPurgeConfirmation: false showPurgeConfirmation: false,
showMoreMenu: false,
downloadingChat: false,
showMissedItemsInfo: false,
/** Timer for showing the "missed items" hint */
timerMissedItems: null
}; };
}, },
mounted() { mounted() {
@ -123,8 +123,8 @@ export default {
let roomMember; let roomMember;
if (this.room) { if (this.room) {
this.room.getMembers().forEach(member => { this.room.getMembers().forEach(member => {
if(this.room.name === member.name) { if (this.room.name === member.name) {
roomMember = member; roomMember = member;
} }
}); });
if (roomMember) { if (roomMember) {
@ -140,8 +140,68 @@ export default {
return null; return null;
}, },
notifications() { 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: { watch: {
room: { 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: { methods: {
setHasShownMissedItemsHint() {
this.$store.commit('setHasShownMissedItemsHint', "1");
this.showMissedItemsInfo = false;
},
onEvent(event) { onEvent(event) {
if (!this.room || event.getRoomId() !== this.room.roomId) { if (!this.room || event.getRoomId() !== this.room.roomId) {
return; // Not for this room return; // Not for this room
@ -169,6 +243,10 @@ export default {
this.$emit("header-click", { event: this.event }); this.$emit("header-click", { event: this.event });
}, },
onAvatarClicked() {
this.$emit("view-room-details", { event: this.event });
},
updateMemberCount() { updateMemberCount() {
if (!this.room) { if (!this.room) {
this.memberCount = 0; this.memberCount = 0;
@ -186,4 +264,45 @@ export default {
<style lang="scss"> <style lang="scss">
@import "@/assets/css/chat.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> </style>

View file

@ -39,17 +39,27 @@
background-color="white" v-on:keyup.enter="$refs.topic.focus()" :disabled="step > steps.INITIAL" autofocus background-color="white" v-on:keyup.enter="$refs.topic.focus()" :disabled="step > steps.INITIAL" autofocus
solo @update:error="updateErrorState"></v-text-field> 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> <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-text-field v-model="roomTopic" v-show="roomName.length > 0" ref="topic" color="black" background-color="white"
v-on:keyup.enter="$refs.create.focus()" :disabled="step > steps.INITIAL" solo></v-text-field> 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 --> <!-- 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 @click.stop="showOptions = !showOptions" v-show="roomName.length > 0" class="options clickable">
<div>{{ $t("new_room.options") }}</div> <div>{{ $t("new_room.options") }}</div>
<v-icon v-if="!showOptions">expand_more</v-icon> <v-icon v-if="!showOptions">expand_more</v-icon>
<v-icon v-else>expand_less</v-icon> <v-icon v-else>expand_less</v-icon>
</div> </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"> <v-card-text class="with-right-label">
<div> <div>
<div class="option-title">{{ $t('room_info.voice_mode') }}</div> <div class="option-title">{{ $t('room_info.voice_mode') }}</div>
@ -58,11 +68,20 @@
<v-switch v-model="useVoiceMode"></v-switch> <v-switch v-model="useVoiceMode"></v-switch>
</v-card-text> </v-card-text>
</v-card> </v-card>
<v-card v-if="$config.experimental_read_only_room" v-show="showOptions" class="room-option account ma-0" flat>
<v-card-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> </template>
<div class="error--text" v-if="roomCreationErrorMsg"> {{ roomCreationErrorMsg }}</div> <div class="error--text" v-if="roomCreationErrorMsg"> {{ roomCreationErrorMsg }}</div>
<v-btn id="btn-room-create" color="black" depressed class="filled-button" @click.stop="onCreate" <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"> <div v-if="status && !enterRoomDialog" class="text-center">
{{ status }} {{ status }}
<v-progress-circular v-if="step == steps.CREATING" indeterminate color="primary" <v-progress-circular v-if="step == steps.CREATING" indeterminate color="primary"
@ -74,91 +93,8 @@
</v-row> </v-row>
</v-container> </v-container>
<v-fade-transition> <interactive-auth ref="interactiveAuth" />
<!-- <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>
<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)" <input id="room-avatar-picker" ref="avatar" type="file" name="avatar" @change="handlePickedAvatar($event)"
accept="image/*" class="d-none" /> accept="image/*" class="d-none" />
<v-dialog v-model="enterRoomDialog" :width="$vuetify.breakpoint.smAndUp ? '50%' : '90%'"> <v-dialog v-model="enterRoomDialog" :width="$vuetify.breakpoint.smAndUp ? '50%' : '90%'">
@ -205,6 +141,7 @@
<script> <script>
import util, { ROOM_TYPE_VOICE_MODE } from "../plugins/utils"; import util, { ROOM_TYPE_VOICE_MODE } from "../plugins/utils";
import InteractiveAuth from './InteractiveAuth.vue';
import rememberMeMixin from "./rememberMeMixin"; import rememberMeMixin from "./rememberMeMixin";
const steps = Object.freeze({ const steps = Object.freeze({
@ -216,6 +153,7 @@ const steps = Object.freeze({
export default { export default {
name: "CreateRoom", name: "CreateRoom",
components: { InteractiveAuth },
mixins: [rememberMeMixin], mixins: [rememberMeMixin],
data() { data() {
return { return {
@ -258,7 +196,9 @@ export default {
roomNameHasError: false, roomNameHasError: false,
roomCreationErrorMsg: "", roomCreationErrorMsg: "",
showOptions: false, showOptions: false,
unencryptedRoom: false,
useVoiceMode: false, useVoiceMode: false,
readOnlyRoom: false,
}; };
}, },
@ -296,7 +236,7 @@ export default {
return false; return false;
} }
return true; return true;
} },
}, },
methods: { methods: {
@ -387,7 +327,17 @@ export default {
visibility: "private", // Not listed! visibility: "private", // Not listed!
name: this.roomName, name: this.roomName,
preset: "public_chat", preset: "public_chat",
initial_state: [ initial_state:
this.unencryptedRoom ? [
{
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: "shared"
}
}
] :
[
{ {
type: "m.room.encryption", type: "m.room.encryption",
state_key: "", state_key: "",
@ -401,7 +351,7 @@ export default {
content: { content: {
history_visibility: "joined" history_visibility: "joined"
} }
} },
], ],
}; };
} else { } else {
@ -446,7 +396,7 @@ export default {
} }
return this.$matrix return this.$matrix
.getLoginPromise() .getLoginPromise(this.$refs.interactiveAuth.registrationFlowHandler)
.then( .then(
function (user) { function (user) {
if (user.is_guest && !hasUser) { if (user.is_guest && !hasUser) {
@ -516,6 +466,20 @@ export default {
} }
}) })
.then(() => { .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 return this.$matrix.matrixClient
.createRoom(createRoomOptions) .createRoom(createRoomOptions)
.then(({ room_id, room_alias }) => { .then(({ room_id, room_alias }) => {
@ -604,7 +568,7 @@ export default {
showAvatarPickerList() { showAvatarPickerList() {
this.$refs.avatar.$refs.input.click(); this.$refs.avatar.$refs.input.click();
}, },
}, }
}; };
</script> </script>

View file

@ -29,7 +29,7 @@
<div class="mt-2" v-if="roomHistoryDescription"> <div class="mt-2" v-if="roomHistoryDescription">
{{ roomHistoryDescription }} {{ roomHistoryDescription }}
</div> </div>
<div class="text-right"> <div class="text-end">
<v-btn id="btn-got-it" text @click.stop="$emit('close')" class="text-transform-0"> <v-btn id="btn-got-it" text @click.stop="$emit('close')" class="text-transform-0">
{{ $t("room_welcome.got_it") }} {{ $t("room_welcome.got_it") }}
</v-btn> </v-btn>

View file

@ -4,7 +4,7 @@
<v-container fluid class="text-center mt-8"> <v-container fluid class="text-center mt-8">
<v-row align="center" justify="center"> <v-row align="center" justify="center">
<v-col class="text-center" cols="auto"> <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-col>
</v-row> </v-row>
</v-container> </v-container>
@ -15,8 +15,6 @@
<RoomList <RoomList
showInvites showInvites
showCreate showCreate
title=""
:invitesTitle="$t('room.room_list_invites')"
v-on:newroom="createRoom" v-on:newroom="createRoom"
/> />
</v-card-text> </v-card-text>
@ -44,12 +42,13 @@
<script> <script>
import RoomList from "../components/RoomList"; import RoomList from "../components/RoomList";
import YouAre from "../components/YouAre.vue"; import YouAre from "../components/YouAre.vue";
import logoMixin from "../components/logoMixin";
export default { export default {
components: { components: {
RoomList, RoomList,
YouAre, YouAre,
}, },
mixins: [logoMixin],
computed: { computed: {
loading() { loading() {
return !this.$matrix.ready; return !this.$matrix.ready;

View file

@ -160,6 +160,11 @@ export default {
position: absolute; position: absolute;
right: 16px; right: 16px;
align-self: center; align-self: center;
[dir="rtl"] & {
left: 16px;
right: unset;
}
} }
} }
</style> </style>

View 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>

View file

@ -80,6 +80,8 @@
</v-col> </v-col>
</v-row> </v-row>
<interactive-auth ref="interactiveAuth" />
<v-btn id="btn-join" class="btn-dark" large @click.stop="handleJoin" :loading="loading" v-if="!currentUser">{{ <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") roomId && roomId.startsWith("@") ? $t("join.enter_room_user") : $t("join.enter_room")
}}</v-btn> }}</v-btn>
@ -121,7 +123,7 @@
<div class="d-flex justify-center align-center mt-9"> <div class="d-flex justify-center align-center mt-9">
<div class="mr-2"> <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>
<div> <div>
<strong>{{ $t("project.name") }}</strong> <strong>{{ $t("project.name") }}</strong>
@ -133,16 +135,18 @@
<script> <script>
import util from "../plugins/utils"; import util from "../plugins/utils";
import InteractiveAuth from './InteractiveAuth.vue';
import LanguageMixin from "./languageMixin"; import LanguageMixin from "./languageMixin";
import rememberMeMixin from "./rememberMeMixin"; import rememberMeMixin from "./rememberMeMixin";
import logoMixin from "./logoMixin";
import SelectLanguageDialog from "./SelectLanguageDialog.vue"; import SelectLanguageDialog from "./SelectLanguageDialog.vue";
export default { export default {
name: "Join", name: "Join",
mixins: [LanguageMixin, rememberMeMixin], mixins: [LanguageMixin, rememberMeMixin, logoMixin],
components: { components: {
SelectLanguageDialog, SelectLanguageDialog,
InteractiveAuth
}, },
data() { data() {
return { return {
@ -269,7 +273,10 @@ export default {
if (roomName && roomName.startsWith("@")) { if (roomName && roomName.startsWith("@")) {
return roomName.substring(roomName.indexOf("@") + 1, lastIndex); 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() { getRoomInfo() {
if (this.roomId.startsWith("#")) { if (this.roomId.startsWith("#")) {
@ -306,6 +313,8 @@ export default {
const room = this.$matrix.getRoom(this.roomId); const room = this.$matrix.getRoom(this.roomId);
if (room) { if (room) {
this.roomName = this.removeHomeServer(room.name || this.roomName); this.roomName = this.removeHomeServer(room.name || this.roomName);
} else {
this.roomName = this.removeHomeServer(this.roomAliasOrId);
} }
this.waitingForInfo = false; this.waitingForInfo = false;
} }
@ -335,7 +344,7 @@ export default {
const hasUser = this.currentUser ? true : false; const hasUser = this.currentUser ? true : false;
var setProfileData = false; var setProfileData = false;
return this.$matrix return this.$matrix
.getLoginPromise() .getLoginPromise(this.$refs.interactiveAuth.registrationFlowHandler)
.then( .then(
function (user) { function (user) {
if (user.is_guest && !hasUser) { if (user.is_guest && !hasUser) {
@ -385,14 +394,14 @@ export default {
return this.$matrix.matrixClient.joinRoom(this.roomId); return this.$matrix.matrixClient.joinRoom(this.roomId);
} }
}) })
.then((ignoredRoom) => { .then((room) => {
this.loading = false; this.loading = false;
this.loadingMessage = null; this.loadingMessage = null;
this.$nextTick(() => { this.$nextTick(() => {
this.$navigation.push( this.$navigation.push(
{ {
name: "Chat", name: "Chat",
params: { roomId: util.sanitizeRoomId(this.roomAliasOrId) }, params: { roomId: util.sanitizeRoomId(room.roomId) },
}, },
-1 -1
); );

View file

@ -5,7 +5,7 @@
<v-row no-gutters> <v-row no-gutters>
<v-col> <v-col>
<v-img <v-img
src="@/assets/logo.svg" :src="logotype"
width="32" width="32"
height="32" height="32"
contain contain
@ -37,10 +37,15 @@
:error="userErrorMessage != null" :error="userErrorMessage != null"
:error-messages="userErrorMessage" :error-messages="userErrorMessage"
required required
v-on:keyup.enter="$refs.password.focus()" v-on:keyup.enter="onUsernameEnter"
v-on:keydown="hasError=false" v-on:keydown="hasError=false"
v-on:blur="onUsernameBlur"
></v-text-field> ></v-text-field>
<div class="error--text" v-if="loadingLoginFlows">Loading login flows...</div>
<v-text-field <v-text-field
v-show="showPasswordField"
prepend-inner-icon="$vuetify.icons.password" prepend-inner-icon="$vuetify.icons.password"
ref="password" ref="password"
v-model="user.password" v-model="user.password"
@ -72,7 +77,7 @@
/> />
<v-btn <v-btn
id="btn-login" id="btn-login"
:disabled="!isValid || loading" :disabled="!isValid || loading || loadingLoginFlows"
color="black" color="black"
depressed depressed
block block
@ -100,10 +105,12 @@
import User from "../models/user"; import User from "../models/user";
import util from "../plugins/utils"; import util from "../plugins/utils";
import rememberMeMixin from "./rememberMeMixin"; import rememberMeMixin from "./rememberMeMixin";
import * as sdk from "matrix-js-sdk";
import logoMixin from "./logoMixin";
export default { export default {
name: "Login", name: "Login",
mixins:[rememberMeMixin], mixins:[rememberMeMixin, logoMixin],
data() { data() {
return { return {
user: new User(this.$config.defaultServer, "", ""), user: new User(this.$config.defaultServer, "", ""),
@ -112,7 +119,11 @@ export default {
message: "", message: "",
userErrorMessage: null, userErrorMessage: null,
passErrorMessage: null, passErrorMessage: null,
hasError: false hasError: false,
currentLoginServer: "",
loadingLoginFlows: false,
loginFlows: null,
showPasswordField: false,
}; };
}, },
computed: { computed: {
@ -124,7 +135,7 @@ export default {
}, },
showCloseButton() { showCloseButton() {
return this.$navigation && this.$navigation.canPop(); return this.$navigation && this.$navigation.canPop();
} },
}, },
created() { created() {
if (this.loggedIn) { if (this.loggedIn) {
@ -158,7 +169,7 @@ export default {
user.normalize(); user.normalize();
this.loading = true; this.loading = true;
this.$store.dispatch("login", user).then( this.$store.dispatch("login", { user }).then(
() => { () => {
if (this.$matrix.currentRoomId) { if (this.$matrix.currentRoomId) {
this.$navigation.push( this.$navigation.push(
@ -175,12 +186,13 @@ export default {
} }
}, },
(error) => { (error) => {
console.error(error);
this.loading = false; this.loading = false;
this.message = this.message =
(error.data && error.data.error) || (error.data && error.data.error) ||
error.message || error.message ||
error.toString(); 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.message = this.$i18n.messages[this.$i18n.locale].login.invalid_message;
this.hasError = true; this.hasError = true;
} }
@ -192,6 +204,45 @@ export default {
handleCreateRoom() { handleCreateRoom() {
this.$navigation.push({ name: "CreateRoom" }); 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> </script>

View 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>

View 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>

View file

@ -1,26 +1,20 @@
<template> <template>
<v-dialog <v-dialog v-model="showDialog" content-class="profile-info-popup" class="ma-0 pa-0">
v-model="showDialog"
content-class="profile-info-popup"
class="ma-0 pa-0"
>
<v-card flat> <v-card flat>
<v-card-text> <v-card-text>
<div class="you-are">{{ $t("profile_info_popup.you_are") }}</div> <div class="you-are">{{ $t("profile_info_popup.you_are") }}</div>
<v-container fluid> <v-container fluid>
<v-row> <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"> <div v-if="$matrix.currentUser.is_guest">
<i18n path="profile_info_popup.identity_temporary" tag="span"> <i18n path="profile_info_popup.identity_temporary" tag="span">
<template v-slot:displayName> <template v-slot:displayName>
<input <input v-model="displayName"
v-model="displayName" @keyup.enter="$event => $event.target.blur()"
@blur=" @blur="
updateDisplayName($event.target.value); updateDisplayName($event.target.value);
editDisplayName = !editDisplayName; editDisplayName = !editDisplayName;
" " @focus="editDisplayName = !editDisplayName" />
@focus="editDisplayName = !editDisplayName"
/>
</template> </template>
</i18n> </i18n>
</div> </div>
@ -29,23 +23,16 @@
<template v-slot:displayName> <template v-slot:displayName>
<input <input
v-model="displayName" v-model="displayName"
@blur=" @keyup.enter="$event => $event.target.blur()"
updateDisplayName($event.target.value); @blur="updateDisplayName($event.target.value);editDisplayName = !editDisplayName;"
editDisplayName = !editDisplayName;
"
@focus="editDisplayName = !editDisplayName" @focus="editDisplayName = !editDisplayName"
/> />
</template> </template>
</i18n> </i18n>
</div> </div>
</v-col> </v-col>
<v-col cols="auto" class="pa-2"> <v-col cols="auto" class="pa-2">
<v-avatar <v-avatar class="avatar-32" size="32" color="#e0e0e0" @click.stop="viewProfile">
class="avatar-32"
size="32"
color="#e0e0e0"
@click.stop="viewProfile"
>
<img v-if="userAvatar" :src="userAvatar" /> <img v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
@ -53,24 +40,6 @@
</v-row> </v-row>
</v-container> </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="more-container">
<div class="want_more"> <div class="want_more">
🙌 {{ $t("profile_info_popup.want_more") }} 🙌 {{ $t("profile_info_popup.want_more") }}
@ -78,7 +47,7 @@
<i18n path="profile_info_popup.powered_by" tag="div"> <i18n path="profile_info_popup.powered_by" tag="div">
<template v-slot:product>{{ product }}</template> <template v-slot:product>{{ product }}</template>
<template v-slot:productLink> <template v-slot:productLink>
<a :href="'//'+productLink">{{ productLink }}</a> <a :href="'//' + productLink">{{ productLink }}</a>
</template> </template>
</i18n> </i18n>
<div class="text-end"> <div class="text-end">
@ -93,16 +62,10 @@
</template> </template>
<script> <script>
import profileInfoMixin from "./profileInfoMixin"; import profileInfoMixin from "./profileInfoMixin";
import ActionRow from "./ActionRow.vue";
import LogoutRoomDialog from './LogoutRoomDialog.vue';
export default { export default {
name: "ProfileInfoPopup", name: "ProfileInfoPopup",
mixins: [profileInfoMixin], mixins: [profileInfoMixin],
components: {
ActionRow,
LogoutRoomDialog
},
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
@ -115,7 +78,6 @@ export default {
return { return {
showDialog: false, showDialog: false,
editDisplayName: false, editDisplayName: false,
showLogoutPopup: false
}; };
}, },
computed: { computed: {
@ -163,28 +125,20 @@ export default {
margin: 0px; margin: 0px;
top: 70px; top: 70px;
right: 10px; right: 10px;
[dir="rtl"] & { [dir="rtl"] & {
right: inherit; right: inherit;
left: 10px; left: unset;
} }
border-radius: 40px; border-radius: 40px;
width: 95%; width: 95%;
&::before {
content: "▲";
position: fixed;
top: 57px;
right: 22px;
[dir="rtl"] & {
left: 22px;
right: inherit;
}
color: white;
}
.you-are { .you-are {
padding-top: 20px; padding-top: 20px;
font-size: 12px; font-size: 12px;
} }
.username { .username {
border-radius: 4px; border-radius: 4px;
background-color: #f5f5f5; background-color: #f5f5f5;
@ -202,19 +156,45 @@ export default {
} }
} }
} }
.more-container { .more-container {
border-radius: 10px; border-radius: 10px;
background-color: #f5f5f5; background-color: #f5f5f5;
padding: 20px; 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 { .want_more {
font-family: "Poppins", sans-serif; font-family: "Poppins", sans-serif;
font-weight: 700; font-weight: 700;
font-size: 13 * $chat-text-size; 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 { .new_room .v-btn__content {
font-family: "Poppins", sans-serif !important; font-family: 'Inter', sans-serif;
font-weight: 700 !important; font-style: normal;
font-size: 13 * $chat-text-size !important; 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')} { @media #{map-get($display-breakpoints, 'lg-and-up')} {
overflow: unset; overflow: unset;
width: $main-desktop-width;; width: $main-desktop-width;
position: absolute; position: absolute;
top: 70px; top: 70px;
right: unset; right: unset;

View file

@ -4,12 +4,6 @@
<!-- Header--> <!-- Header-->
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0"> <v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
<v-row class="chat-header-row flex-nowrap"> <v-row class="chat-header-row flex-nowrap">
<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"> <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"> <div class="room-name-inline text-truncate" :title="room.name">
{{ room.name }} {{ room.name }}

View file

@ -5,8 +5,8 @@
<v-btn <v-btn
id="btn-back" id="btn-back"
text text
:class="$navigation && $navigation.canPop() ? 'v-visible' : 'v-hidden'" :class="(($navigation && $navigation.canPop()) || $matrix.currentRoomId) ? 'v-visible' : 'v-hidden'"
@click.stop="$navigation.pop" @click.stop="goBack()"
> >
<v-icon>arrow_back</v-icon> <v-icon>arrow_back</v-icon>
<span class="d-none d-sm-block">{{ $t("menu.back") }}</span> <span class="d-none d-sm-block">{{ $t("menu.back") }}</span>
@ -165,9 +165,9 @@
</v-card-text> </v-card-text>
</v-card> </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-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>
<div class="option-title">{{ $t('room_info.voice_mode') }}</div> <div class="option-title">{{ $t('room_info.voice_mode') }}</div>
<div class="option-text">{{ $t('room_info.voice_mode_info') }}</div> <div class="option-text">{{ $t('room_info.voice_mode_info') }}</div>
@ -176,6 +176,15 @@
v-model="useVoiceMode" v-model="useVoiceMode"
></v-switch> ></v-switch>
</v-card-text> </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>
<v-card class="members ma-3" flat> <v-card class="members ma-3" flat>
@ -254,11 +263,12 @@
id="btn-purge-room" id="btn-purge-room"
v-if="userCanPurgeRoom" v-if="userCanPurgeRoom"
color="red" color="red"
depressed fab
class="filled-button" class="filled-button"
@click.stop="showPurgeConfirmation = true" @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>
<div class="build-version"> <div class="build-version">
@ -388,6 +398,14 @@ export default {
this.$matrix.matrixClient.setRoomTag(this.room.roomId, "ui_options", options); 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) { handler(ignoredNewVal, ignoredOldVal) {
console.log("RoomInfo: Current room changed"); console.log("RoomInfo: Current room changed");
this.updateMembers(); 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 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! // TODO - following power level comparisons assume that default power levels are used in the room!
isAdmin(member) { isAdmin(member) {
return member.powerLevelNorm > 50; return member.powerLevelNorm > 50;
@ -599,7 +630,7 @@ export default {
}, },
/** /**
* Return true if WE can make the member an admin * Return true if WE can make the member an admin
* @param member * @param member
*/ */
canMakeAdmin(ignoredmember) { canMakeAdmin(ignoredmember) {
if (this.room) { if (this.room) {
@ -612,7 +643,7 @@ export default {
/** /**
* Return true if WE can make the member a moderator * Return true if WE can make the member a moderator
* @param member * @param member
*/ */
canMakeModerator(ignoredmember) { canMakeModerator(ignoredmember) {
if (this.room) { if (this.room) {
@ -620,11 +651,11 @@ export default {
const me = this.room.getMember(myUserId); const me = this.room.getMember(myUserId);
return me && this.isAdmin(me); return me && this.isAdmin(me);
} }
return false; return false;
}, },
/** /**
* Return true if WE can "unmake" the member a moderator * Return true if WE can "unmake" the member a moderator
* @param member * @param member
*/ */
canRevokeModerator(member) { canRevokeModerator(member) {
if (this.room) { if (this.room) {
@ -632,7 +663,7 @@ export default {
const me = this.room.getMember(myUserId); const me = this.room.getMember(myUserId);
return me && this.isAdmin(me) && me.powerLevel > member.powerLevel; return me && this.isAdmin(me) && me.powerLevel > member.powerLevel;
} }
return false; return false;
}, },
makeAdmin(member) { makeAdmin(member) {
if (this.room) { if (this.room) {
@ -658,6 +689,23 @@ export default {
if (this.room) { if (this.room) {
this.$matrix.banUser(this.room.roomId, member.userId) 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
);
}
} }
}, },
}; };

View file

@ -3,52 +3,16 @@
class="room-info-bottom-sheet" class="room-info-bottom-sheet"
:halfY="0.12" :halfY="0.12"
ref="sheet" ref="sheet"
:showCloseButton="false"
> >
<div class="room-info-sheet" ref="roomInfoSheetContent"> <div class="room-info-sheet" ref="roomInfoSheetContent">
<div class="text-center current-room"> <room-list v-on:close="close" v-on:newroom="createRoom" :showCreate="true" />
<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" />
</div> </div>
</BottomSheet> </BottomSheet>
</template> </template>
<script> <script>
import BottomSheet from "./BottomSheet"; import BottomSheet from "./BottomSheet";
import RoomAvatarPicker from "./RoomAvatarPicker";
import RoomList from "./RoomList"; import RoomList from "./RoomList";
import roomInfoMixin from "./roomInfoMixin"; import roomInfoMixin from "./roomInfoMixin";
@ -58,7 +22,6 @@ export default {
components: { components: {
BottomSheet, BottomSheet,
RoomList, RoomList,
RoomAvatarPicker,
}, },
methods: { methods: {
open() { open() {

View file

@ -1,70 +1,58 @@
<template> <template>
<v-list dense class="room-list"> <v-list dense class="room-list">
<div class="h4">{{ title }}</div> <v-list-item-group @change="roomChange" color="primary">
<v-list-item-group v-model="currentRoomId" color="primary">
<v-list-item v-if="showCreate" @click.stop="$emit('newroom')"> <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-content>
<v-list-item-title class="new-room">{{ <v-list-item-title class="room-list-new-room">{{
$t("menu.new_room") $t("menu.new_room")
}}</v-list-item-title> }}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- invites --> <!-- invites -->
<v-list-item <v-list-item :disabled="roomsProcessing[room.roomId]" v-for="room in invitedRooms" :key="room.roomId"
:disabled="roomsProcessing[room.roomId]" :value="room.roomId" class="room-list-room">
v-for="room in invitedRooms" <v-list-item-avatar size="42" color="#d9d9d9">
:key="room.roomId" <v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" />
:value="room.roomId"
>
<v-list-item-avatar size="40" color="#e0e0e0">
<v-img v-if="room.avatar" :src="room.avatar" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
room.name.substring(0, 1).toUpperCase() room.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <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-subtitle>{{ room.topic }}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-btn <v-btn id="btn-accept" class="filled-button" depressed color="black" @click.stop="acceptInvitation(room)">{{
id="btn-accept" $t("menu.join") }}</v-btn>
class="filled-button" <v-btn id="btn-reject" class="filled-button" color="black" @click.stop="rejectInvitation(room)" text>{{
depressed $t("menu.ignore") }}</v-btn>
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-action>
</v-list-item> </v-list-item>
<v-list-item <v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room.roomId" class="room-list-room">
v-for="room in joinedRooms" <v-list-item-avatar size="42" color="#d9d9d9">
:key="room.roomId" <v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" />
:value="room.roomId"
>
<v-list-item-avatar size="40" color="#e0e0e0">
<v-img v-if="room.avatar" :src="room.avatar" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
room.name.substring(0, 1).toUpperCase() room.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
</v-list-item-avatar> </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-content>
<v-list-item-title>{{ room.name }}</v-list-item-title> <v-list-item-title class="room-list-name">{{ room.name }}
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle> <!-- <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-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>
</v-list-item-group> </v-list-item-group>
</v-list> </v-list>
@ -93,7 +81,6 @@ export default {
}, },
data: () => ({ data: () => ({
currentRoomId: null,
/** A list of rooms currently processing some operation, like "join" or "reject" */ /** A list of rooms currently processing some operation, like "join" or "reject" */
roomsProcessing: {}, roomsProcessing: {},
}), }),
@ -106,8 +93,27 @@ export default {
return this.sortItemsOnName(this.$matrix.joinedRooms); return this.sortItemsOnName(this.$matrix.joinedRooms);
}, },
}, },
methods: { 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) { sortItemsOnName(items) {
if (items == null) { if (items == null) {
return []; return [];
@ -166,11 +172,17 @@ export default {
Vue.delete(this.roomsProcessing, room.roomId); Vue.delete(this.roomsProcessing, room.roomId);
}); });
}, },
},
watch: { isPublic(room) {
currentRoomId() { return this.$matrix.getRoomJoinRule(room) === "public"
if (this.currentRoomId == null) { },
isDirect(room) {
return this.$matrix.isDirectRoom(room);
},
roomChange(roomId) {
if (roomId == null || roomId == undefined) {
// Ignore, this is caused by "new room" etc. // Ignore, this is caused by "new room" etc.
return; return;
} }
@ -178,11 +190,11 @@ export default {
this.$navigation.push( this.$navigation.push(
{ {
name: "Chat", name: "Chat",
params: { roomId: util.sanitizeRoomId(this.currentRoomId) }, params: { roomId: util.sanitizeRoomId(roomId) },
}, },
-1 -1
); );
}, }
}, },
}; };
</script> </script>

View file

@ -2,10 +2,10 @@
<transition name="grow" mode="out-in"> <transition name="grow" mode="out-in">
<div <div
v-show="show" v-show="show"
:class="{ 'voice-recorder': true, ptt: ptt, row: !ptt }" :class="{ 'voice-recorder': true, ptt: usePTT, row: !usePTT }"
ref="vrroot" 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-row align="center" class="mt-3">
<v-col cols="4" align="center"> <v-col cols="4" align="center">
<v-btn v-show="state == states.RECORDED" icon @click.stop="redo"> <v-btn v-show="state == states.RECORDED" icon @click.stop="redo">
@ -71,7 +71,7 @@
{{ recordingTime }} {{ recordingTime }}
</div> </div>
</v-col> </v-col>
<v-col cols="6" v-if="ptt"> <v-col cols="6" v-if="usePTT">
<div class="swipe-info"> <div class="swipe-info">
&lt;&lt; {{ $t("voice_recorder.swipe_to_cancel") }} &lt;&lt; {{ $t("voice_recorder.swipe_to_cancel") }}
</div> </div>
@ -146,7 +146,7 @@
</div> </div>
<VoiceRecorderLock <VoiceRecorderLock
v-show="state == states.RECORDING && ptt" v-show="state == states.RECORDING && usePTT"
:style="lockButtonStyle" :style="lockButtonStyle"
:isLocked="recordingLocked" :isLocked="recordingLocked"
/> />
@ -209,6 +209,9 @@ export default {
errorMessage: null, errorMessage: null,
recorder: null, recorder: null,
previewPlayer: null, previewPlayer: null,
wakeLock: null,
maxRecordingLength: 300, // In seconds
forceNonPTTMode: false,
}; };
}, },
watch: { watch: {
@ -244,13 +247,14 @@ export default {
} }
}, },
show(val) { show(val) {
this.forceNonPTTMode = false;
if (val) { if (val) {
// Add listeners // Add listeners
this.state = State.INITIAL; this.state = State.INITIAL;
this.errorMessage = null; this.errorMessage = null;
this.recordedFile = null; this.recordedFile = null;
this.recordingTime = String.fromCharCode(160); this.recordingTime = String.fromCharCode(160);
if (this.ptt) { if (this.usePTT) {
document.addEventListener("mouseup", this.mouseUp, false); document.addEventListener("mouseup", this.mouseUp, false);
document.addEventListener("mousemove", this.mouseMove, false); document.addEventListener("mousemove", this.mouseMove, false);
document.addEventListener("touchend", this.mouseUp, false); document.addEventListener("touchend", this.mouseUp, false);
@ -288,6 +292,9 @@ export default {
} }
}, },
computed: { computed: {
usePTT() {
return this.ptt && !this.forceNonPTTMode;
},
lockButtonStyle() { lockButtonStyle() {
/** /**
Calculate where to show the lock button (it should be at the same X-coord as the) 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.recordStartedAt = Date.now();
this.startRecordTimer(); this.startRecordTimer();
}) })
.then(async () => {
this.aquireWakeLock();
})
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
if (e && e.name == "NotAllowedError") { if (e && e.name == "NotAllowedError") {
@ -374,25 +384,65 @@ export default {
this.state = State.ERROR; 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() { cancelRecording() {
if(this.recorder) { if(this.recorder) {
this.recorder.stop(); this.recorder.stop();
this.recorder = null; this.recorder = null;
} }
this.releaseWakeLock();
this.state = State.INITIAL; this.state = State.INITIAL;
this.close(); this.close();
}, },
pauseRecording() { 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.state = State.RECORDED;
this.stopRecordTimer(); this.stopRecordTimer();
this.releaseWakeLock();
this.getFile(false); this.getFile(false);
this.playRecordedSound();
}, },
stopRecording() { stopRecording() {
this.state = State.RECORDED; this.state = State.RECORDED;
this.stopRecordTimer(); this.stopRecordTimer();
this.releaseWakeLock();
this.recordingTime = String.fromCharCode(160); // nbsp; this.recordingTime = String.fromCharCode(160); // nbsp;
this.close(); this.close();
this.getFile(true); this.getFile(true);
this.playRecordedSound();
}, },
redo() { redo() {
this.state = State.INITIAL; this.state = State.INITIAL;
@ -431,6 +481,10 @@ export default {
this.recordingTime = util.formatRecordDuration( this.recordingTime = util.formatRecordDuration(
now - this.recordStartedAt now - this.recordStartedAt
); );
// Auto-stop?
if ((now - this.recordStartedAt) >= (1000 * this.maxRecordingLength) && this.state == State.RECORDING) {
this.pauseRecording();
}
}, 500); }, 500);
}, },
stopRecordTimer() { stopRecordTimer() {

View file

@ -94,6 +94,15 @@ export default {
CreatePollDialog, CreatePollDialog,
}, },
methods: { methods: {
showOnlyUserStatusMessages() {
// We say that if you can redact events, you are allowed to create polls.
// NOTE!!! This assumes that there is a property named "room" on THIS.
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
let isModerator =
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
const show = this.$config.show_status_messages;
return show === "never" || (show === "moderators" && !isModerator)
},
showDayMarkerBeforeEvent(event) { showDayMarkerBeforeEvent(event) {
const idx = this.events.indexOf(event); const idx = this.events.indexOf(event);
if (idx <= 0) { if (idx <= 0) {
@ -132,10 +141,12 @@ export default {
return ContactKicked; return ContactKicked;
} }
return ContactLeave; return ContactLeave;
} else if (event.getContent().membership == "invite") { } else if (!this.showOnlyUserStatusMessages()) {
return ContactInvited; if (event.getContent().membership == "invite") {
} else if (event.getContent().membership == "ban") { return ContactInvited;
return ContactBanned; } else if (event.getContent().membership == "ban") {
return ContactBanned;
}
} }
break; break;
@ -203,34 +214,64 @@ export default {
} }
case "m.room.create": case "m.room.create":
return RoomCreated; if (!this.showOnlyUserStatusMessages()) {
return RoomCreated;
}
break;
case "m.room.canonical_alias": case "m.room.canonical_alias":
return RoomAliased; if (!this.showOnlyUserStatusMessages()) {
return RoomAliased;
}
break;
case "m.room.name": case "m.room.name":
return RoomNameChanged; if (!this.showOnlyUserStatusMessages()) {
return RoomNameChanged;
}
break;
case "m.room.topic": case "m.room.topic":
return RoomTopicChanged; if (!this.showOnlyUserStatusMessages()) {
return RoomTopicChanged;
}
break;
case "m.room.avatar": case "m.room.avatar":
return RoomAvatarChanged; if (!this.showOnlyUserStatusMessages()) {
return RoomAvatarChanged;
}
break;
case "m.room.history_visibility": case "m.room.history_visibility":
return RoomHistoryVisibility; if (!this.showOnlyUserStatusMessages()) {
return RoomHistoryVisibility;
}
break;
case "m.room.join_rules": case "m.room.join_rules":
return RoomJoinRules; if (!this.showOnlyUserStatusMessages()) {
return RoomJoinRules;
}
break;
case "m.room.power_levels": case "m.room.power_levels":
return RoomPowerLevelsChanged; if (!this.showOnlyUserStatusMessages()) {
return RoomPowerLevelsChanged;
}
break;
case "m.room.guest_access": case "m.room.guest_access":
return RoomGuestAccessChanged; if (!this.showOnlyUserStatusMessages()) {
return RoomGuestAccessChanged;
}
break;
case "m.room.encryption": case "m.room.encryption":
return RoomEncrypted; if (!this.showOnlyUserStatusMessages()) {
return RoomEncrypted;
}
break;
case "m.poll.start": case "m.poll.start":
case "org.matrix.msc3381.poll.start": case "org.matrix.msc3381.poll.start":

View file

@ -0,0 +1,10 @@
export default {
computed: {
logotype() {
if (this.$config.logo) {
return this.$config.logo;
}
return require("@/assets/logo.svg");
}
}
}

View file

@ -15,20 +15,23 @@
</v-avatar> </v-avatar>
<!-- SLOT FOR CONTENT --> <!-- SLOT FOR CONTENT -->
<slot></slot> <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-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
<v-icon>more_vert</v-icon> <v-icon>more_vert</v-icon>
</v-btn> </v-btn>
</div> </div>
<QuickReactions :event="event" :timelineSet="timelineSet" /> <QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy :room="room" :event="event"/>
</div> </div>
</template> </template>
<script> <script>
import SeenBy from "./SeenBy.vue";
import messageMixin from "./messageMixin"; import messageMixin from "./messageMixin";
export default { export default {
mixins: [messageMixin], mixins: [messageMixin],
components: { SeenBy }
}; };
</script> </script>

View file

@ -8,8 +8,7 @@
<div class="status">{{ event.status }}</div> <div class="status">{{ event.status }}</div>
</div> </div>
<QuickReactions :event="event" :timelineSet="timelineSet" /> <div class="op-button" ref="opbutton" v-if="!event.isRedacted() && !$matrix.currentRoomIsReadOnlyForUser">
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
<v-btn id="btn-show-menu" icon @click.stop="showContextMenu($refs.opbutton)"> <v-btn id="btn-show-menu" icon @click.stop="showContextMenu($refs.opbutton)">
<v-icon>more_vert</v-icon> <v-icon>more_vert</v-icon>
</v-btn> </v-btn>
@ -25,14 +24,18 @@
<img v-if="userAvatar" :src="userAvatar" /> <img v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text headline">{{ userAvatarLetter }}</span> <span v-else class="white--text headline">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy :room="room" :event="event"/>
</div> </div>
</template> </template>
<script> <script>
import SeenBy from "./SeenBy.vue";
import messageMixin from "./messageMixin"; import messageMixin from "./messageMixin";
export default { export default {
mixins: [messageMixin], mixins: [messageMixin],
components: { SeenBy }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -1,13 +1,67 @@
<template> <template>
<div class="quick-reaction-container" v-show="reactions"> <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)"> <div
{{ name }} <span class="quick-reaction-count">{{ value.length }}</span> class="emoji"
</span> 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> </div>
</template> </template>
<script> <script>
import messageOperationsMixin from "./messageOperationsMixin";
export default { export default {
mixins: [messageOperationsMixin],
props: { props: {
event: { event: {
type: Object, type: Object,
@ -25,7 +79,9 @@ export default {
data() { data() {
return { return {
reactionMap: {}, reactionMap: {},
reactions: null reactions: null,
REACTION_LIMIT: 5,
showAllReaction: false
} }
}, },
mounted() { mounted() {
@ -38,6 +94,14 @@ export default {
this.reactions.off('Relations.add', this.onAddRelation); 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: { methods: {
onRelationsCreated() { onRelationsCreated() {
this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction'); this.reactions = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), 'm.annotation', 'm.reaction');

View 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>

View file

@ -98,13 +98,15 @@ export default {
const relatesTo = this.event.getWireContent()["m.relates_to"]; const relatesTo = this.event.getWireContent()["m.relates_to"];
if (relatesTo && relatesTo["m.in_reply_to"]) { if (relatesTo && relatesTo["m.in_reply_to"]) {
const content = this.event.getContent(); const content = this.event.getContent();
const lines = content.body.split("\n").reverse(); if ('body' in content) {
while (lines.length && !lines[0].startsWith("> ")) lines.shift(); const lines = content.body.split("\n").reverse() || [];
// Reply fallback has a blank line after it, so remove it to prevent leading newline while (lines.length && !lines[0].startsWith("> ")) lines.shift();
if (lines[0] === "") lines.shift(); // Reply fallback has a blank line after it, so remove it to prevent leading newline
const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, ""); if (lines[0] === "") lines.shift();
if (text) { const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, "");
return text; if (text) {
return text;
}
} }
if (this.inReplyToEvent) { if (this.inReplyToEvent) {
@ -122,13 +124,14 @@ export default {
const relatesTo = this.event.getWireContent()["m.relates_to"]; const relatesTo = this.event.getWireContent()["m.relates_to"];
if (relatesTo && relatesTo["m.in_reply_to"]) { if (relatesTo && relatesTo["m.in_reply_to"]) {
const content = this.event.getContent(); const content = this.event.getContent();
if ('body' in content) {
// Remove the new text and strip "> " from the old original text // Remove the new text and strip "> " from the old original text
const lines = content.body.split("\n"); const lines = content.body.split("\n");
while (lines.length && lines[0].startsWith("> ")) lines.shift(); while (lines.length && lines[0].startsWith("> ")) lines.shift();
// Reply fallback has a blank line after it, so remove it to prevent leading newline // Reply fallback has a blank line after it, so remove it to prevent leading newline
if (lines[0] === "") lines.shift(); if (lines[0] === "") lines.shift();
return lines.join("\n"); return lines.join("\n");
}
} }
return this.event.getContent().body; return this.event.getContent().body;
}, },

View file

@ -1,6 +1,5 @@
import Vue from 'vue' import Vue from 'vue'
import App from './App.vue' import App from './App.vue'
import vuetify from './plugins/vuetify';
import store from './store' import store from './store'
import i18n from './plugins/lang'; import i18n from './plugins/lang';
import router from './router' import router from './router'
@ -15,6 +14,7 @@ import VueResize from 'vue-resize';
import 'vue-resize/dist/vue-resize.css'; import 'vue-resize/dist/vue-resize.css';
import VueClipboard from 'vue-clipboard2' import VueClipboard from 'vue-clipboard2'
import VueSanitize from "vue-sanitize"; import VueSanitize from "vue-sanitize";
import createVuetify from './plugins/vuetify';
var defaultOptions = VueSanitize.defaults; var defaultOptions = VueSanitize.defaults;
defaultOptions.disallowedTagsMode = "recursiveEscape"; defaultOptions.disallowedTagsMode = "recursiveEscape";
@ -26,11 +26,18 @@ Vue.config.productionTip = false
Vue.use(VueResize); Vue.use(VueResize);
Vue.use(VEmojiPicker); Vue.use(VEmojiPicker);
Vue.use(matrix, { store: store, i18n: i18n }); 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(analytics);
Vue.use(VueClipboard); Vue.use(VueClipboard);
const vuetify = createVuetify(config);
// Add bubble functionality to custom events. // Add bubble functionality to custom events.
// From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events // From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events
Vue.use((Vue) => { Vue.use((Vue) => {
@ -161,7 +168,7 @@ Vue.directive('longTap', {
Vue.use(navigation, router); Vue.use(navigation, router);
new Vue({ const vueInstance = new Vue({
vuetify, vuetify,
store, store,
i18n, i18n,
@ -170,4 +177,10 @@ new Vue({
config, config,
analytics, analytics,
render: h => h(App) 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');
});

View file

@ -28,11 +28,12 @@ var _browserCanRecordAudioF = function () {
} }
var _browserCanRecordAudio = _browserCanRecordAudioF(); var _browserCanRecordAudio = _browserCanRecordAudioF();
class AbortablePromise extends Promise { class UploadPromise extends Promise {
constructor(executor) { constructor(executor) {
const aborter = { const aborter = {
aborted: false, aborted: false,
abortablePromise: undefined, abortablePromise: undefined,
matrixClient: undefined,
} }
const normalExecutor = function (resolve, reject) { const normalExecutor = function (resolve, reject) {
@ -42,8 +43,9 @@ class AbortablePromise extends Promise {
super(normalExecutor); super(normalExecutor);
this.abort = () => { this.abort = () => {
aborter.aborted = true; aborter.aborted = true;
if (aborter.abortablePromise) { if (aborter.abortablePromise && aborter.matrixClient) {
aborter.abortablePromise.abort(); aborter.matrixClient.cancelUpload(aborter.abortablePromise);
aborter.matrixClient = undefined;
aborter.abortablePromise = undefined; aborter.abortablePromise = undefined;
} }
}; };
@ -268,7 +270,7 @@ class Util {
resolve(true); resolve(true);
}) })
.catch(err => { .catch(err => {
console.log("Image send error: ", err); console.log("Send error: ", err);
if (err && err.name == "UnknownDeviceError") { if (err && err.name == "UnknownDeviceError") {
console.log("Unknown devices. Mark as known before retrying."); console.log("Unknown devices. Mark as known before retrying.");
var setAsKnownPromises = []; var setAsKnownPromises = [];
@ -290,7 +292,18 @@ class Util {
Promise.all(setAsKnownPromises) Promise.all(setAsKnownPromises)
.then(() => { .then(() => {
// All devices now marked as "known", try to resend // 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) => { .then((result) => {
console.log("Message sent: ", result); console.log("Message sent: ", result);
resolve(true); resolve(true);
@ -309,7 +322,7 @@ class Util {
} }
sendImage(matrixClient, roomId, file, onUploadProgress) { sendImage(matrixClient, roomId, file, onUploadProgress) {
return new AbortablePromise((resolve, reject, aborter) => { return new UploadPromise((resolve, reject, aborter) => {
const abortionController = aborter; const abortionController = aborter;
var reader = new FileReader(); var reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
@ -355,6 +368,7 @@ class Util {
if (!matrixClient.isRoomEncrypted(roomId)) { if (!matrixClient.isRoomEncrypted(roomId)) {
// Not encrypted. // Not encrypted.
abortionController.matrixClient = matrixClient;
abortionController.abortablePromise = matrixClient.uploadContent(data, opts); abortionController.abortablePromise = matrixClient.uploadContent(data, opts);
abortionController.abortablePromise abortionController.abortablePromise
.then((response) => { .then((response) => {
@ -548,24 +562,23 @@ class Util {
isChildVisible(parentNode, childNode) { isChildVisible(parentNode, childNode) {
const rect1 = parentNode.getBoundingClientRect(); const rect1 = parentNode.getBoundingClientRect();
const rect2 = childNode.getBoundingClientRect(); const rect2 = childNode.getBoundingClientRect();
var overlap = !(rect1.right < rect2.left || var overlap = !(rect1.right <= rect2.left ||
rect1.left > rect2.right || rect1.left >= rect2.right ||
rect1.bottom < rect2.top || rect1.bottom <= rect2.top ||
rect1.top > rect2.bottom) rect1.top >= rect2.bottom)
return overlap; return overlap;
} }
findOneVisibleElement(parentNode) { findOneVisibleElement(parentNode) {
let start = 0; let start = 0;
let end = parentNode.children.length - 1; let end = parentNode.children.length - 1;
let top = parentNode.scrollTop;
while (start <= end) { while (start <= end) {
let middle = Math.floor((start + end) / 2); let middle = Math.floor((start + end) / 2);
let childNode = parentNode.children[middle]; let childNode = parentNode.children[middle];
if (this.isChildVisible(parentNode, childNode)) { if (this.isChildVisible(parentNode, childNode)) {
// found the key // found the key
return middle; return middle;
} else if (childNode.offsetTop < top) { } else if (childNode.getBoundingClientRect().top <= parentNode.getBoundingClientRect().top) {
// continue searching to the right // continue searching to the right
start = middle + 1; start = middle + 1;
} else { } else {

View file

@ -14,12 +14,22 @@ function importAll(r) {
} }
importAll(require.context('@/assets/icons/', true, /\.vue$/)); importAll(require.context('@/assets/icons/', true, /\.vue$/));
Vue.use(Vuetify); Vue.use(Vuetify);
export default new Vuetify({ export default function(ignoredconfig) {
icons: { return new Vuetify({
iconfont: 'md', icons: {
values: icons, iconfont: 'md',
}, values: icons,
}); },
options: {
customProperties: true
},
theme: {
options: {
customProperties: true,
},
dark: false,
}
});
}

View file

@ -1,13 +1,14 @@
export default { export default {
install(Vue, defaultServerFromLocation) { install(Vue, defaultServerFromLocation, onloaded) {
var config = Vue.observable(require('@/assets/config.json')); var config = Vue.observable(require('@/assets/config.json'));
const getRuntimeConfig = async () => { const getRuntimeConfig = () => {
const runtimeConfig = await fetch('./config.json'); return fetch('./config.json').then((res) => res.json()).catch(err => {
return await runtimeConfig.json() console.error("Failed to get config:", err);
return {};
});
} }
config.promise = getRuntimeConfig(); config.promise = getRuntimeConfig().then((json) => {
config.promise.then(function (json) {
// Reactively use all the config values // Reactively use all the config values
for (const key of Object.keys(json)) { for (const key of Object.keys(json)) {
Vue.set(config, key, json[key]); Vue.set(config, key, json[key]);
@ -16,6 +17,12 @@ export default {
if (!json.defaultServer) { if (!json.defaultServer) {
Vue.set(config, "defaultServer", defaultServerFromLocation); Vue.set(config, "defaultServer", defaultServerFromLocation);
} }
// Tell callback we are done loading runtime config
if (onloaded) {
onloaded(config);
}
return config;
}); });
Vue.prototype.$config = config; Vue.prototype.$config = config;
} }

View file

@ -37,6 +37,7 @@ export default {
userDisplayName: null, userDisplayName: null,
userAvatar: null, userAvatar: null,
currentRoom: null, currentRoom: null,
currentRoomIsReadOnlyForUser: false,
currentRoomBeingPurged: false, currentRoomBeingPurged: false,
notificationCount: 0, notificationCount: 0,
}; };
@ -95,6 +96,16 @@ export default {
this.currentRoom = this.getRoom(roomId); 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: { methods: {
@ -102,35 +113,59 @@ export default {
console.log("create crypto store"); console.log("create crypto store");
return new LocalStorageCryptoStore(this.$store.getters.storage); return new LocalStorageCryptoStore(this.$store.getters.storage);
}, },
login(user) { login(user, registrationFlowHandler) {
const tempMatrixClient = sdk.createClient(user.home_server); const tempMatrixClient = sdk.createClient({baseUrl: user.home_server, idBaseUrl: this.$config.identityServer});
var promiseLogin; var promiseLogin;
const self = this; const self = this;
if (user.access_token) { if (user.access_token) {
// Logged in on "real" account // Logged in on "real" account
promiseLogin = Promise.resolve(user); promiseLogin = Promise.resolve(user);
} else if (user.is_guest && !user.user_id) { } else if (user.is_guest && (!user.user_id || user.registration_session)) {
// Generate random username and password. We don't user REAL matrix // Generate random username and password. We don't user REAL matrix
// guest accounts because 1. They are not allowed to post media, 2. They // guest accounts because 1. They are not allowed to post media, 2. They
// can not use avatars and 3. They can not seamlessly be upgraded to real accounts. // can not use avatars and 3. They can not seamlessly be upgraded to real accounts.
// //
// Instead, we use an ILAG approach, Improved Landing as Guest. // Instead, we use an ILAG approach, Improved Landing as Guest.
const user = util.randomUser(this.$config.userIdPrefix); const userId = user.registration_session ? user.user_id : util.randomUser(this.$config.userIdPrefix);
const pass = util.randomPass(); 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 promiseLogin = tempMatrixClient
.register(user, pass, null, { .register(userId, pass, user.registration_session || null, {
type: "m.login.dummy", type: "m.login.dummy",
initial_device_display_name: this.$config.appName, initial_device_display_name: this.$config.appName,
}) })
.then((response) => { .then((response) => {
console.log("Response", response); return extractAndSaveUser(response);
var u = Object.assign({}, response); })
u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. .catch(error => {
u.password = pass; if (registrationFlowHandler && error.httpStatus == 401 && error.data) {
u.is_guest = true; const registrationSession = error.data.session;
this.$store.commit("setUser", u);
return u; // 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 { } else {
var data = { var data = {
@ -284,11 +319,11 @@ export default {
* Will use a real account, if we have one, otherwise will create * Will use a real account, if we have one, otherwise will create
* a random account. * a random account.
*/ */
getLoginPromise() { getLoginPromise(registrationFlowHandler) {
if (this.ready) { if (this.ready) {
return Promise.resolve(this.currentUser); 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) { addMatrixClientListeners(client) {
@ -355,6 +390,14 @@ export default {
} }
} }
break; 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(); this.updateNotificationCount();
}, },
@ -422,10 +465,14 @@ export default {
} }
}); });
Vue.set(this, "rooms", updatedRooms); Vue.set(this, "rooms", updatedRooms);
const currentRoom = this.getRoom(this.$store.state.currentRoomId);
if (this.currentRoom != currentRoom) { const resolvedId = (this.currentRoomId && this.currentRoomId.startsWith("#")) ? this.matrixClient.resolveRoomAlias(this.currentRoomId).then(r => r.room_id) : Promise.resolve(this.currentRoomId);
this.currentRoom = currentRoom; resolvedId.then(roomId => {
} const currentRoom = this.getRoom(roomId);
if (this.currentRoom != currentRoom) {
this.currentRoom = currentRoom;
}
}).catch(ignorederror => {});
}, },
setCurrentRoomId(roomId) { setCurrentRoomId(roomId) {
@ -492,10 +539,11 @@ export default {
leaveRoom(roomId) { leaveRoom(roomId) {
return this.matrixClient.leave(roomId, undefined).then(() => { return this.matrixClient.leave(roomId, undefined).then(() => {
this.$store.commit("setCurrentRoomId", null);
this.rooms = this.rooms.filter((room) => { this.rooms = this.rooms.filter((room) => {
room.roomId != roomId; room.roomId != roomId;
}); });
this.matrixClient.store.removeRoom(roomId); //this.matrixClient.store.removeRoom(roomId);
//this.matrixClient.forget(roomId, true, undefined); //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: * Purge the room with the given id! This means:
* - Make room invite only * - Make room invite only
@ -761,7 +872,17 @@ export default {
type: "m.room.history_visibility", type: "m.room.history_visibility",
state_key: "", state_key: "",
content: { 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 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) { on(event, handler) {
if (this.matrixClient) { if (this.matrixClient) {
this.matrixClient.on(event, handler); this.matrixClient.on(event, handler);
@ -856,7 +991,7 @@ export default {
return this.matrixClient; return this.matrixClient;
}); });
} else { } else {
const tempMatrixClient = sdk.createClient(this.$config.defaultServer); const tempMatrixClient = sdk.createClient({baseUrl: this.$config.defaultServer});
var tempUserString = this.$store.state.tempuser; var tempUserString = this.$store.state.tempuser;
var tempUser = null; var tempUser = null;
if (tempUserString) { if (tempUserString) {

View file

@ -40,6 +40,7 @@ const vuexPersistLocalStorage = new VuexPersist({
return { return {
language: state.language, language: state.language,
currentRoomId: state.currentRoomId, currentRoomId: state.currentRoomId,
hasShownMissedItemsHint: state.hasShownMissedItemsHint,
}; };
} else { } else {
return {}; return {};
@ -55,6 +56,7 @@ const vuexPersistSessionStorage = new VuexPersist({
return { return {
language: state.language, language: state.language,
currentRoomId: state.currentRoomId, currentRoomId: state.currentRoomId,
hasShownMissedItemsHint: state.hasShownMissedItemsHint,
}; };
} else { } else {
return {}; return {};
@ -93,11 +95,14 @@ export default new Vuex.Store({
}, },
setUseLocalStorage(state, useLocalStorage) { setUseLocalStorage(state, useLocalStorage) {
state.useLocalStorage = useLocalStorage; state.useLocalStorage = useLocalStorage;
},
setHasShownMissedItemsHint(state, flag) {
state.hasShownMissedItemsHint = flag;
} }
}, },
actions: { actions: {
login({ commit }, user) { login({ commit }, { user, registrationFlowHandler }) {
return this._vm.$matrix.login(user).then( return this._vm.$matrix.login(user, registrationFlowHandler).then(
user => { user => {
commit('loginSuccess', user); commit('loginSuccess', user);
return Promise.resolve(user); return Promise.resolve(user);