Merge branch 'develop' into event-tile-preview-fixes

This commit is contained in:
Robin Townsend 2021-06-16 18:27:10 -04:00
commit 2750d64495
95 changed files with 2264 additions and 895 deletions

38
.github/workflows/develop.yml vendored Normal file
View file

@ -0,0 +1,38 @@
name: Develop jobs
on:
push:
branches: [develop]
pull_request:
branches: [develop]
jobs:
end-to-end:
runs-on: ubuntu-latest
container: vectorim/element-web-ci-e2etests-env:latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: End-to-End tests
run: ./scripts/ci/end-to-end-tests.sh
- name: Archive logs
uses: actions/upload-artifact@v2
with:
path: |
test/end-to-end-tests/logs/**/*
test/end-to-end-tests/synapse/installations/consent/homeserver.log
retention-days: 14
- name: Download previous benchmark data
uses: actions/cache@v1
with:
path: ./cache
key: ${{ runner.os }}-benchmark
- name: Store benchmark result
uses: matrix-org/github-action-benchmark@jsperfentry-1
with:
tool: 'jsperformanceentry'
output-file-path: test/end-to-end-tests/performance-entries.json
fail-on-alert: false
comment-on-alert: false
# Only temporary to monitor where failures occur
alert-comment-cc-users: '@gsouquet'
github-token: ${{ secrets.DEPLOY_GH_PAGES }}
auto-push: ${{ github.ref == 'refs/heads/develop' }}

View file

@ -55,7 +55,6 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"await-lock": "^2.1.0", "await-lock": "^2.1.0",
"blueimp-canvas-to-blob": "^3.28.0",
"browser-encrypt-attachment": "^0.3.0", "browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"cheerio": "^1.0.0-rc.9", "cheerio": "^1.0.0-rc.9",
@ -88,18 +87,16 @@
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"qs": "^6.9.6",
"re-resizable": "^6.9.0", "re-resizable": "^6.9.0",
"react": "^16.14.0", "react": "^17.0.2",
"react-beautiful-dnd": "^4.0.1", "react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.14.0", "react-dom": "^17.0.2",
"react-focus-lock": "^2.5.0", "react-focus-lock": "^2.5.0",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"rfc4648": "^1.4.0", "rfc4648": "^1.4.0",
"sanitize-html": "^2.3.2", "sanitize-html": "^2.3.2",
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0", "url": "^0.11.0",
"what-input": "^5.2.10", "what-input": "^5.2.10",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
@ -147,7 +144,7 @@
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.6", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
"eslint": "7.18.0", "eslint": "7.18.0",
"eslint-config-matrix-org": "^0.2.0", "eslint-config-matrix-org": "^0.2.0",
"eslint-plugin-babel": "^5.3.1", "eslint-plugin-babel": "^5.3.1",
@ -160,9 +157,9 @@
"jest-environment-jsdom-sixteen": "^1.0.3", "jest-environment-jsdom-sixteen": "^1.0.3",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"matrix-mock-request": "^1.2.3", "matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2", "matrix-react-test-utils": "^0.2.3",
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
"react-test-renderer": "^16.14.0", "react-test-renderer": "^17.0.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stylelint": "^13.9.0", "stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^20.0.0",

View file

@ -76,6 +76,7 @@
@import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_ForwardDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss";

View file

@ -328,7 +328,8 @@ $SpaceRoomViewInnerWidth: 428px;
font-size: $font-15px; font-size: $font-15px;
margin-top: 12px; margin-top: 12px;
margin-bottom: 16px; margin-bottom: 16px;
white-space: pre; white-space: pre-wrap;
word-wrap: break-word;
} }
> hr { > hr {

View file

@ -0,0 +1,159 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ForwardDialog {
width: 520px;
color: $primary-fg-color;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
height: 80vh;
> h3 {
margin: 0 0 6px;
color: $secondary-fg-color;
font-size: $font-12px;
font-weight: $font-semi-bold;
line-height: $font-15px;
}
> .mx_ForwardDialog_preview {
max-height: 30%;
flex-shrink: 0;
overflow: scroll;
div {
pointer-events: none;
}
.mx_EventTile_msgOption {
display: none;
}
// When forwarding messages from encrypted rooms, EventTile will complain
// that our preview is unencrypted, which doesn't actually matter
.mx_EventTile_e2eIcon_unencrypted {
display: none;
}
// We also hide download links to not encourage users to try interacting
.mx_MFileBody_download {
display: none;
}
}
> hr {
width: 100%;
border: none;
border-top: 1px solid $input-border-color;
margin: 12px 0;
}
> .mx_ForwardList {
display: contents;
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
}
.mx_ForwardList_content {
flex-grow: 1;
}
.mx_ForwardList_noResults {
display: block;
margin-top: 24px;
}
.mx_ForwardList_results {
&:not(:first-child) {
margin-top: 24px;
}
.mx_ForwardList_entry {
display: flex;
justify-content: space-between;
height: 32px;
padding: 6px;
border-radius: 8px;
&:hover {
background-color: $groupFilterPanel-bg-color;
}
.mx_ForwardList_roomButton {
display: flex;
margin-right: 12px;
min-width: 0;
.mx_DecoratedRoomAvatar {
margin-right: 12px;
}
.mx_ForwardList_entry_name {
font-size: $font-15px;
line-height: 30px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 12px;
}
}
.mx_ForwardList_sendButton {
position: relative;
&:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel {
// Hide the "Send" label while preserving button size
visibility: hidden;
}
.mx_ForwardList_sendIcon, .mx_NotificationBadge {
position: absolute;
}
.mx_NotificationBadge {
// Match the failed to send indicator's color with the disabled button
background-color: $button-danger-disabled-fg-color;
}
&.mx_ForwardList_sending .mx_ForwardList_sendIcon {
background-color: $button-primary-bg-color;
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: 14px;
width: 14px;
height: 14px;
}
&.mx_ForwardList_sent .mx_ForwardList_sendIcon {
background-color: $button-primary-bg-color;
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: 14px;
width: 14px;
height: 14px;
}
}
}
}
}
}

View file

@ -17,6 +17,9 @@ limitations under the License.
.mx_InviteDialog_addressBar { .mx_InviteDialog_addressBar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
// for the user section gets weird.
margin: 8px 45px 0 0;
.mx_InviteDialog_editor { .mx_InviteDialog_editor {
flex: 1; flex: 1;
@ -73,7 +76,7 @@ limitations under the License.
} }
.mx_InviteDialog_section { .mx_InviteDialog_section {
padding-bottom: 10px; padding-bottom: 4px;
h3 { h3 {
font-size: $font-12px; font-size: $font-12px;
@ -82,6 +85,14 @@ limitations under the License.
text-transform: uppercase; text-transform: uppercase;
} }
> p {
margin: 0;
}
> span {
color: $primary-fg-color;
}
.mx_InviteDialog_subname { .mx_InviteDialog_subname {
margin-bottom: 10px; margin-bottom: 10px;
margin-top: -10px; // HACK: Positioning with margins is bad margin-top: -10px; // HACK: Positioning with margins is bad
@ -90,6 +101,63 @@ limitations under the License.
} }
} }
.mx_InviteDialog_section_hidden_suggestions_disclaimer {
padding: 8px 0 16px 0;
font-size: $font-14px;
> span {
color: $primary-fg-color;
font-weight: 600;
}
> p {
margin: 0;
}
}
.mx_InviteDialog_footer {
border-top: 1px solid $input-border-color;
> h3 {
margin: 12px 0;
font-size: $font-12px;
color: $muted-fg-color;
font-weight: bold;
text-transform: uppercase;
}
.mx_InviteDialog_footer_link {
display: flex;
justify-content: space-between;
border-radius: 4px;
border: solid 1px $light-fg-color;
padding: 8px;
> a {
text-decoration: none;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
}
}
.mx_InviteDialog_footer_link_copy {
flex-shrink: 0;
cursor: pointer;
margin-left: 20px;
display: inherit;
> div {
mask-image: url($copy-button-url);
background-color: $message-action-bar-fg-color;
margin-left: 5px;
width: 20px;
height: 20px;
background-repeat: no-repeat;
}
}
}
.mx_InviteDialog_roomTile { .mx_InviteDialog_roomTile {
cursor: pointer; cursor: pointer;
padding: 5px 10px; padding: 5px 10px;
@ -142,6 +210,7 @@ limitations under the License.
.mx_InviteDialog_roomTile_nameStack { .mx_InviteDialog_roomTile_nameStack {
display: inline-block; display: inline-block;
overflow: hidden;
} }
.mx_InviteDialog_roomTile_name { .mx_InviteDialog_roomTile_name {
@ -157,6 +226,13 @@ limitations under the License.
margin-left: 7px; margin-left: 7px;
} }
.mx_InviteDialog_roomTile_name,
.mx_InviteDialog_roomTile_userId {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mx_InviteDialog_roomTile_time { .mx_InviteDialog_roomTile_time {
text-align: right; text-align: right;
font-size: $font-12px; font-size: $font-12px;
@ -212,22 +288,29 @@ limitations under the License.
.mx_InviteDialog { .mx_InviteDialog {
// Prevent the dialog from jumping around randomly when elements change. // Prevent the dialog from jumping around randomly when elements change.
height: 590px; height: 600px;
padding-left: 20px; // the design wants some padding on the left padding-left: 20px; // the design wants some padding on the left
display: flex;
flex-direction: column;
.mx_InviteDialog_content {
overflow: hidden;
}
} }
.mx_InviteDialog_userSections { .mx_InviteDialog_userSections {
margin-top: 10px; margin-top: 4px;
overflow-y: auto; overflow-y: auto;
padding-right: 45px; padding: 0 45px 4px 0;
height: 455px; // mx_InviteDialog's height minus some for the upper elements height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements
} }
// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar .mx_InviteDialog_hasFooter .mx_InviteDialog_userSections {
// for the user section gets weird. height: calc(100% - 175px);
.mx_InviteDialog_helpText, }
.mx_InviteDialog_addressBar {
margin-right: 45px; .mx_InviteDialog_helpText {
margin: 0;
} }
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {

View file

@ -50,7 +50,8 @@ limitations under the License.
margin-left: 20px; margin-left: 20px;
display: inherit; display: inherit;
} }
.mx_ShareDialog_matrixto_copy > div { .mx_ShareDialog_matrixto_copy::after {
content: "";
mask-image: url($copy-button-url); mask-image: url($copy-button-url);
background-color: $message-action-bar-fg-color; background-color: $message-action-bar-fg-color;
margin-left: 5px; margin-left: 5px;

View file

@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_SenderProfile_name { .mx_SenderProfile_displayName {
font-weight: 600; font-weight: 600;
} }
.mx_SenderProfile_mxid {
font-weight: 600;
font-size: 1.1rem;
margin-left: 5px;
opacity: 0.5; // Match mx_TextualEvent
}

View file

@ -32,4 +32,59 @@ limitations under the License.
margin-right: 6px; margin-right: 6px;
} }
} }
.mx_PinnedMessagesCard_empty {
display: flex;
height: 100%;
> div {
height: max-content;
text-align: center;
margin: auto 40px;
.mx_PinnedMessagesCard_MessageActionBar {
pointer-events: none;
display: flex;
height: 32px;
line-height: $font-24px;
border-radius: 8px;
background: $primary-bg-color;
border: 1px solid $input-border-color;
padding: 1px;
width: max-content;
margin: 0 auto;
box-sizing: border-box;
.mx_MessageActionBar_maskButton {
display: inline-block;
position: relative;
}
.mx_MessageActionBar_optionsButton {
background: $roomlist-button-bg-color;
border-radius: 6px;
z-index: 1;
&::after {
background-color: $primary-fg-color;
}
}
}
> h2 {
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
margin-top: 24px;
margin-bottom: 20px;
}
> span {
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
}
}
}
} }

View file

@ -16,6 +16,7 @@ limitations under the License.
*/ */
$left-gutter: 64px; $left-gutter: 64px;
$hover-select-border: 4px;
.mx_EventTile { .mx_EventTile {
max-width: 100%; max-width: 100%;
@ -142,8 +143,7 @@ $left-gutter: 64px;
} }
.mx_EventTile_selected > div > a > .mx_MessageTimestamp { .mx_EventTile_selected > div > a > .mx_MessageTimestamp {
left: 3px; left: calc(-$hover-select-border);
width: auto;
} }
.mx_EventTile:hover .mx_MessageActionBar, .mx_EventTile:hover .mx_MessageActionBar,
@ -158,7 +158,7 @@ $left-gutter: 64px;
*/ */
.mx_EventTile_selected > .mx_EventTile_line { .mx_EventTile_selected > .mx_EventTile_line {
border-left: $accent-color 4px solid; border-left: $accent-color 4px solid;
padding-left: 60px; padding-left: calc($left-gutter - $hover-select-border);
background-color: $event-selected-color; background-color: $event-selected-color;
} }
@ -171,8 +171,12 @@ $left-gutter: 64px;
} }
} }
.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px);
}
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
padding-left: 78px; padding-left: calc($left-gutter + 18px - $hover-select-border);
} }
.mx_EventTile:hover .mx_EventTile_line, .mx_EventTile:hover .mx_EventTile_line,
@ -408,7 +412,7 @@ $left-gutter: 64px;
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
padding-left: 60px; padding-left: calc($left-gutter - $hover-select-border);
} }
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
@ -426,7 +430,7 @@ $left-gutter: 64px;
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
padding-left: 78px; padding-left: calc($left-gutter + 18px - $hover-select-border);
} }
/* End to end encryption stuff */ /* End to end encryption stuff */
@ -438,7 +442,7 @@ $left-gutter: 64px;
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
width: $MessageTimestamp_width_hover; left: calc(-$hover-select-border);
} }
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)

View file

@ -24,10 +24,6 @@ $left-gutter: 64px;
margin-left: $left-gutter; margin-left: $left-gutter;
} }
> .mx_EventTile_line {
padding-left: $left-gutter;
}
> .mx_EventTile_avatar { > .mx_EventTile_avatar {
position: absolute; position: absolute;
} }
@ -43,10 +39,6 @@ $left-gutter: 64px;
line-height: $font-22px; line-height: $font-22px;
} }
} }
.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px);
}
} }
/* Compact layout overrides */ /* Compact layout overrides */

View file

@ -178,7 +178,7 @@ $irc-line-height: $font-18px;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
> .mx_SenderProfile_name { > .mx_SenderProfile_displayName {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
min-width: var(--name-width); min-width: var(--name-width);
@ -207,7 +207,7 @@ $irc-line-height: $font-18px;
background: transparent; background: transparent;
> span { > span {
> .mx_SenderProfile_name { > .mx_SenderProfile_displayName {
min-width: inherit; min-width: inherit;
} }
} }

View file

@ -542,6 +542,7 @@ export default class CallHandler extends EventEmitter {
if (newMappedRoomId !== mappedRoomId) { if (newMappedRoomId !== mappedRoomId) {
this.removeCallForRoom(mappedRoomId); this.removeCallForRoom(mappedRoomId);
mappedRoomId = newMappedRoomId; mappedRoomId = newMappedRoomId;
console.log("Moving call to room " + mappedRoomId);
this.calls.set(mappedRoomId, call); this.calls.set(mappedRoomId, call);
this.emit(CallHandlerEvent.CallChangeRoom, call); this.emit(CallHandlerEvent.CallChangeRoom, call);
} }
@ -607,6 +608,7 @@ export default class CallHandler extends EventEmitter {
} }
private removeCallForRoom(roomId: string) { private removeCallForRoom(roomId: string) {
console.log("Removing call for room ", roomId);
this.calls.delete(roomId); this.calls.delete(roomId);
this.emit(CallHandlerEvent.CallsChanged, this.calls); this.emit(CallHandlerEvent.CallsChanged, this.calls);
} }
@ -680,6 +682,7 @@ export default class CallHandler extends EventEmitter {
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
const call = MatrixClientPeg.get().createCall(mappedRoomId); const call = MatrixClientPeg.get().createCall(mappedRoomId);
console.log("Adding call for room ", roomId);
this.calls.set(roomId, call); this.calls.set(roomId, call);
this.emit(CallHandlerEvent.CallsChanged, this.calls); this.emit(CallHandlerEvent.CallsChanged, this.calls);
if (transferee) { if (transferee) {
@ -812,6 +815,7 @@ export default class CallHandler extends EventEmitter {
} }
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
console.log("Adding call for room ", mappedRoomId);
this.calls.set(mappedRoomId, call) this.calls.set(mappedRoomId, call)
this.emit(CallHandlerEvent.CallsChanged, this.calls); this.emit(CallHandlerEvent.CallsChanged, this.calls);
this.setCallListeners(call); this.setCallListeners(call);

View file

@ -28,8 +28,6 @@ import encrypt from "browser-encrypt-attachment";
import extractPngChunks from "png-chunks-extract"; import extractPngChunks from "png-chunks-extract";
import Spinner from "./components/views/elements/Spinner"; import Spinner from "./components/views/elements/Spinner";
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
import "blueimp-canvas-to-blob";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
import { import {

View file

@ -24,13 +24,6 @@ import {sleep} from "./utils/promise";
import RoomViewStore from "./stores/RoomViewStore"; import RoomViewStore from "./stores/RoomViewStore";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
// polyfill textencoder if necessary
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
let TextEncoder = window.TextEncoder;
if (!TextEncoder) {
TextEncoder = TextEncodingUtf8.TextEncoder;
}
const INACTIVITY_TIME = 20; // seconds const INACTIVITY_TIME = 20; // seconds
const HEARTBEAT_INTERVAL = 5_000; // ms const HEARTBEAT_INTERVAL = 5_000; // ms
const SESSION_UPDATE_INTERVAL = 60; // seconds const SESSION_UPDATE_INTERVAL = 60; // seconds

View file

@ -21,153 +21,161 @@ import SettingsStore from "./settings/SettingsStore";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore"; import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
function textForMemberEvent(ev) { // These functions are frequently used just to check whether an event has
// any text to display at all. For this reason they return deferred values
// to avoid the expense of looking up translations when they're not needed.
function textForMemberEvent(ev): () => string | null {
// XXX: SYJS-16 "sender is sometimes null for join messages" // XXX: SYJS-16 "sender is sometimes null for join messages"
const senderName = ev.sender ? ev.sender.name : ev.getSender(); const senderName = ev.sender ? ev.sender.name : ev.getSender();
const targetName = ev.target ? ev.target.name : ev.getStateKey(); const targetName = ev.target ? ev.target.name : ev.getStateKey();
const prevContent = ev.getPrevContent(); const prevContent = ev.getPrevContent();
const content = ev.getContent(); const content = ev.getContent();
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : ''; const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
switch (content.membership) { switch (content.membership) {
case 'invite': { case 'invite': {
const threePidContent = content.third_party_invite; const threePidContent = content.third_party_invite;
if (threePidContent) { if (threePidContent) {
if (threePidContent.display_name) { if (threePidContent.display_name) {
return _t('%(targetName)s accepted the invitation for %(displayName)s.', { return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
targetName, targetName,
displayName: threePidContent.display_name, displayName: threePidContent.display_name,
}); });
} else { } else {
return _t('%(targetName)s accepted an invitation.', {targetName}); return () => _t('%(targetName)s accepted an invitation.', {targetName});
} }
} else { } else {
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
} }
} }
case 'ban': case 'ban':
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason; return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
case 'join': case 'join':
if (prevContent && prevContent.membership === 'join') { if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', { return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
oldDisplayName: prevContent.displayname, oldDisplayName: prevContent.displayname,
displayName: content.displayname, displayName: content.displayname,
}); });
} else if (!prevContent.displayname && content.displayname) { } else if (!prevContent.displayname && content.displayname) {
return _t('%(senderName)s set their display name to %(displayName)s.', { return () => _t('%(senderName)s set their display name to %(displayName)s.', {
senderName: ev.getSender(), senderName: ev.getSender(),
displayName: content.displayname, displayName: content.displayname,
}); });
} else if (prevContent.displayname && !content.displayname) { } else if (prevContent.displayname && !content.displayname) {
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', { return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
senderName, senderName,
oldDisplayName: prevContent.displayname, oldDisplayName: prevContent.displayname,
}); });
} else if (prevContent.avatar_url && !content.avatar_url) { } else if (prevContent.avatar_url && !content.avatar_url) {
return _t('%(senderName)s removed their profile picture.', {senderName}); return () => _t('%(senderName)s removed their profile picture.', {senderName});
} else if (prevContent.avatar_url && content.avatar_url && } else if (prevContent.avatar_url && content.avatar_url &&
prevContent.avatar_url !== content.avatar_url) { prevContent.avatar_url !== content.avatar_url) {
return _t('%(senderName)s changed their profile picture.', {senderName}); return () => _t('%(senderName)s changed their profile picture.', {senderName});
} else if (!prevContent.avatar_url && content.avatar_url) { } else if (!prevContent.avatar_url && content.avatar_url) {
return _t('%(senderName)s set a profile picture.', {senderName}); return () => _t('%(senderName)s set a profile picture.', {senderName});
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
// This is a null rejoin, it will only be visible if the Labs option is enabled // This is a null rejoin, it will only be visible if the Labs option is enabled
return _t("%(senderName)s made no change.", {senderName}); return () => _t("%(senderName)s made no change.", {senderName});
} else { } else {
return ""; return null;
} }
} else { } else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
return _t('%(targetName)s joined the room.', {targetName}); return () => _t('%(targetName)s joined the room.', {targetName});
} }
case 'leave': case 'leave':
if (ev.getSender() === ev.getStateKey()) { if (ev.getSender() === ev.getStateKey()) {
if (prevContent.membership === "invite") { if (prevContent.membership === "invite") {
return _t('%(targetName)s rejected the invitation.', {targetName}); return () => _t('%(targetName)s rejected the invitation.', {targetName});
} else { } else {
return _t('%(targetName)s left the room.', {targetName}); return () => _t('%(targetName)s left the room.', {targetName});
} }
} else if (prevContent.membership === "ban") { } else if (prevContent.membership === "ban") {
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
} else if (prevContent.membership === "invite") { } else if (prevContent.membership === "invite") {
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
senderName, senderName,
targetName, targetName,
}) + ' ' + reason; }) + ' ' + getReason();
} else if (prevContent.membership === "join") { } else if (prevContent.membership === "join") {
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason; return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
} else { } else {
return ""; return null;
} }
} }
} }
function textForTopicEvent(ev) { function textForTopicEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
senderDisplayName, senderDisplayName,
topic: ev.getContent().topic, topic: ev.getContent().topic,
}); });
} }
function textForRoomNameEvent(ev) { function textForRoomNameEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName}); return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
} }
if (ev.getPrevContent().name) { if (ev.getPrevContent().name) {
return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', { return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
senderDisplayName, senderDisplayName,
oldRoomName: ev.getPrevContent().name, oldRoomName: ev.getPrevContent().name,
newRoomName: ev.getContent().name, newRoomName: ev.getContent().name,
}); });
} }
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', { return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
senderDisplayName, senderDisplayName,
roomName: ev.getContent().name, roomName: ev.getContent().name,
}); });
} }
function textForTombstoneEvent(ev) { function textForTombstoneEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName}); return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
} }
function textForJoinRulesEvent(ev) { function textForJoinRulesEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().join_rule) { switch (ev.getContent().join_rule) {
case "public": case "public":
return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName}); return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
senderDisplayName,
});
case "invite": case "invite":
return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName}); return () => _t('%(senderDisplayName)s made the room invite only.', {
senderDisplayName,
});
default: default:
// The spec supports "knock" and "private", however nothing implements these. // The spec supports "knock" and "private", however nothing implements these.
return _t('%(senderDisplayName)s changed the join rule to %(rule)s', { return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
senderDisplayName, senderDisplayName,
rule: ev.getContent().join_rule, rule: ev.getContent().join_rule,
}); });
} }
} }
function textForGuestAccessEvent(ev) { function textForGuestAccessEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().guest_access) { switch (ev.getContent().guest_access) {
case "can_join": case "can_join":
return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName}); return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
case "forbidden": case "forbidden":
return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName}); return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
default: default:
// There's no other options we can expect, however just for safety's sake we'll do this. // There's no other options we can expect, however just for safety's sake we'll do this.
return _t('%(senderDisplayName)s changed guest access to %(rule)s', { return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
senderDisplayName, senderDisplayName,
rule: ev.getContent().guest_access, rule: ev.getContent().guest_access,
}); });
} }
} }
function textForRelatedGroupsEvent(ev) { function textForRelatedGroupsEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const groups = ev.getContent().groups || []; const groups = ev.getContent().groups || [];
const prevGroups = ev.getPrevContent().groups || []; const prevGroups = ev.getPrevContent().groups || [];
@ -175,17 +183,17 @@ function textForRelatedGroupsEvent(ev) {
const removed = prevGroups.filter((g) => !groups.includes(g)); const removed = prevGroups.filter((g) => !groups.includes(g));
if (added.length && !removed.length) { if (added.length && !removed.length) {
return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', { return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
senderDisplayName, senderDisplayName,
groups: added.join(', '), groups: added.join(', '),
}); });
} else if (!added.length && removed.length) { } else if (!added.length && removed.length) {
return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', { return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
senderDisplayName, senderDisplayName,
groups: removed.join(', '), groups: removed.join(', '),
}); });
} else if (added.length && removed.length) { } else if (added.length && removed.length) {
return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' + return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
'%(oldGroups)s in this room.', { '%(oldGroups)s in this room.', {
senderDisplayName, senderDisplayName,
newGroups: added.join(', '), newGroups: added.join(', '),
@ -193,11 +201,11 @@ function textForRelatedGroupsEvent(ev) {
}); });
} else { } else {
// Don't bother rendering this change (because there were no changes) // Don't bother rendering this change (because there were no changes)
return ''; return null;
} }
} }
function textForServerACLEvent(ev) { function textForServerACLEvent(ev): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const prevContent = ev.getPrevContent(); const prevContent = ev.getPrevContent();
const current = ev.getContent(); const current = ev.getContent();
@ -207,11 +215,11 @@ function textForServerACLEvent(ev) {
allow_ip_literals: !(prevContent.allow_ip_literals === false), allow_ip_literals: !(prevContent.allow_ip_literals === false),
}; };
let text = ""; let getText = null;
if (prev.deny.length === 0 && prev.allow.length === 0) { if (prev.deny.length === 0 && prev.allow.length === 0) {
text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName}); getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
} else { } else {
text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName}); getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
} }
if (!Array.isArray(current.allow)) { if (!Array.isArray(current.allow)) {
@ -220,13 +228,15 @@ function textForServerACLEvent(ev) {
// If we know for sure everyone is banned, mark the room as obliterated // If we know for sure everyone is banned, mark the room as obliterated
if (current.allow.length === 0) { if (current.allow.length === 0) {
return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used."); return () => getText() + " " +
_t("🎉 All servers are banned from participating! This room can no longer be used.");
} }
return text; return getText;
} }
function textForMessageEvent(ev) { function textForMessageEvent(ev): () => string | null {
return () => {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
let message = senderDisplayName + ': ' + ev.getContent().body; let message = senderDisplayName + ': ' + ev.getContent().body;
if (ev.getContent().msgtype === "m.emote") { if (ev.getContent().msgtype === "m.emote") {
@ -235,9 +245,10 @@ function textForMessageEvent(ev) {
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName}); message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
} }
return message; return message;
};
} }
function textForCanonicalAliasEvent(ev) { function textForCanonicalAliasEvent(ev): () => string | null {
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const oldAlias = ev.getPrevContent().alias; const oldAlias = ev.getPrevContent().alias;
const oldAltAliases = ev.getPrevContent().alt_aliases || []; const oldAltAliases = ev.getPrevContent().alt_aliases || [];
@ -248,96 +259,100 @@ function textForCanonicalAliasEvent(ev) {
if (!removedAltAliases.length && !addedAltAliases.length) { if (!removedAltAliases.length && !addedAltAliases.length) {
if (newAlias) { if (newAlias) {
return _t('%(senderName)s set the main address for this room to %(address)s.', { return () => _t('%(senderName)s set the main address for this room to %(address)s.', {
senderName: senderName, senderName: senderName,
address: ev.getContent().alias, address: ev.getContent().alias,
}); });
} else if (oldAlias) { } else if (oldAlias) {
return _t('%(senderName)s removed the main address for this room.', { return () => _t('%(senderName)s removed the main address for this room.', {
senderName: senderName, senderName: senderName,
}); });
} }
} else if (newAlias === oldAlias) { } else if (newAlias === oldAlias) {
if (addedAltAliases.length && !removedAltAliases.length) { if (addedAltAliases.length && !removedAltAliases.length) {
return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', { return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
senderName: senderName, senderName: senderName,
addresses: addedAltAliases.join(", "), addresses: addedAltAliases.join(", "),
count: addedAltAliases.length, count: addedAltAliases.length,
}); });
} if (removedAltAliases.length && !addedAltAliases.length) { } if (removedAltAliases.length && !addedAltAliases.length) {
return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', { return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
senderName: senderName, senderName: senderName,
addresses: removedAltAliases.join(", "), addresses: removedAltAliases.join(", "),
count: removedAltAliases.length, count: removedAltAliases.length,
}); });
} if (removedAltAliases.length && addedAltAliases.length) { } if (removedAltAliases.length && addedAltAliases.length) {
return _t('%(senderName)s changed the alternative addresses for this room.', { return () => _t('%(senderName)s changed the alternative addresses for this room.', {
senderName: senderName, senderName: senderName,
}); });
} }
} else { } else {
// both alias and alt_aliases where modified // both alias and alt_aliases where modified
return _t('%(senderName)s changed the main and alternative addresses for this room.', { return () => _t('%(senderName)s changed the main and alternative addresses for this room.', {
senderName: senderName, senderName: senderName,
}); });
} }
// in case there is no difference between the two events, // in case there is no difference between the two events,
// say something as we can't simply hide the tile from here // say something as we can't simply hide the tile from here
return _t('%(senderName)s changed the addresses for this room.', { return () => _t('%(senderName)s changed the addresses for this room.', {
senderName: senderName, senderName: senderName,
}); });
} }
function textForCallAnswerEvent(event) { function textForCallAnswerEvent(event): () => string | null {
return () => {
const senderName = event.sender ? event.sender.name : _t('Someone'); const senderName = event.sender ? event.sender.name : _t('Someone');
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported; return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
};
} }
function textForCallHangupEvent(event) { function textForCallHangupEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : _t('Someone'); const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent(); const eventContent = event.getContent();
let reason = ""; let getReason = () => "";
if (!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
reason = _t('(not supported by this browser)'); getReason = () => _t('(not supported by this browser)');
} else if (eventContent.reason) { } else if (eventContent.reason) {
if (eventContent.reason === "ice_failed") { if (eventContent.reason === "ice_failed") {
// We couldn't establish a connection at all // We couldn't establish a connection at all
reason = _t('(could not connect media)'); getReason = () => _t('(could not connect media)');
} else if (eventContent.reason === "ice_timeout") { } else if (eventContent.reason === "ice_timeout") {
// We established a connection but it died // We established a connection but it died
reason = _t('(connection failed)'); getReason = () => _t('(connection failed)');
} else if (eventContent.reason === "user_media_failed") { } else if (eventContent.reason === "user_media_failed") {
// The other side couldn't open capture devices // The other side couldn't open capture devices
reason = _t("(their device couldn't start the camera / microphone)"); getReason = () => _t("(their device couldn't start the camera / microphone)");
} else if (eventContent.reason === "unknown_error") { } else if (eventContent.reason === "unknown_error") {
// An error code the other side doesn't have a way to express // An error code the other side doesn't have a way to express
// (as opposed to an error code they gave but we don't know about, // (as opposed to an error code they gave but we don't know about,
// in which case we show the error code) // in which case we show the error code)
reason = _t("(an error occurred)"); getReason = () => _t("(an error occurred)");
} else if (eventContent.reason === "invite_timeout") { } else if (eventContent.reason === "invite_timeout") {
reason = _t('(no answer)'); getReason = () => _t('(no answer)');
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") { } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
// workaround for https://github.com/vector-im/element-web/issues/5178 // workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is // it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :( // interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623 // https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore) // Also the correct hangup code as of VoIP v1 (with underscore)
reason = ''; getReason = () => '';
} else { } else {
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
} }
} }
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason; return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
} }
function textForCallRejectEvent(event) { function textForCallRejectEvent(event): () => string | null {
return () => {
const senderName = event.sender ? event.sender.name : _t('Someone'); const senderName = event.sender ? event.sender.name : _t('Someone');
return _t('%(senderName)s declined the call.', {senderName}); return _t('%(senderName)s declined the call.', {senderName});
};
} }
function textForCallInviteEvent(event) { function textForCallInviteEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : _t('Someone'); const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event? // FIXME: Find a better way to determine this from the event?
let isVoice = true; let isVoice = true;
if (event.getContent().offer && event.getContent().offer.sdp && if (event.getContent().offer && event.getContent().offer.sdp &&
@ -350,48 +365,55 @@ function textForCallInviteEvent(event) {
// can have a hard time translating those strings. In an effort to make translations easier // can have a hard time translating those strings. In an effort to make translations easier
// and more accurate, we break out the string-based variables to a couple booleans. // and more accurate, we break out the string-based variables to a couple booleans.
if (isVoice && isSupported) { if (isVoice && isSupported) {
return _t("%(senderName)s placed a voice call.", {senderName}); return () => _t("%(senderName)s placed a voice call.", {
senderName: getSenderName(),
});
} else if (isVoice && !isSupported) { } else if (isVoice && !isSupported) {
return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName}); return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
senderName: getSenderName(),
});
} else if (!isVoice && isSupported) { } else if (!isVoice && isSupported) {
return _t("%(senderName)s placed a video call.", {senderName}); return () => _t("%(senderName)s placed a video call.", {
senderName: getSenderName(),
});
} else if (!isVoice && !isSupported) { } else if (!isVoice && !isSupported) {
return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName}); return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
senderName: getSenderName(),
});
} }
} }
function textForThreePidInviteEvent(event) { function textForThreePidInviteEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
if (!isValid3pidInvite(event)) { if (!isValid3pidInvite(event)) {
const targetDisplayName = event.getPrevContent().display_name || _t("Someone"); return () => _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
senderName, senderName,
targetDisplayName, targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
}); });
} }
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', { return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
senderName, senderName,
targetDisplayName: event.getContent().display_name, targetDisplayName: event.getContent().display_name,
}); });
} }
function textForHistoryVisibilityEvent(event) { function textForHistoryVisibilityEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
switch (event.getContent().history_visibility) { switch (event.getContent().history_visibility) {
case 'invited': case 'invited':
return _t('%(senderName)s made future room history visible to all room members, ' return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they are invited.', {senderName}); + 'from the point they are invited.', {senderName});
case 'joined': case 'joined':
return _t('%(senderName)s made future room history visible to all room members, ' return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they joined.', {senderName}); + 'from the point they joined.', {senderName});
case 'shared': case 'shared':
return _t('%(senderName)s made future room history visible to all room members.', {senderName}); return () => _t('%(senderName)s made future room history visible to all room members.', {senderName});
case 'world_readable': case 'world_readable':
return _t('%(senderName)s made future room history visible to anyone.', {senderName}); return () => _t('%(senderName)s made future room history visible to anyone.', {senderName});
default: default:
return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', { return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
senderName, senderName,
visibility: event.getContent().history_visibility, visibility: event.getContent().history_visibility,
}); });
@ -399,11 +421,11 @@ function textForHistoryVisibilityEvent(event) {
} }
// Currently will only display a change if a user's power level is changed // Currently will only display a change if a user's power level is changed
function textForPowerEvent(event) { function textForPowerEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
if (!event.getPrevContent() || !event.getPrevContent().users || if (!event.getPrevContent() || !event.getPrevContent().users ||
!event.getContent() || !event.getContent().users) { !event.getContent() || !event.getContent().users) {
return ''; return null;
} }
const userDefault = event.getContent().users_default || 0; const userDefault = event.getContent().users_default || 0;
// Construct set of userIds // Construct set of userIds
@ -418,38 +440,38 @@ function textForPowerEvent(event) {
if (users.indexOf(userId) === -1) users.push(userId); if (users.indexOf(userId) === -1) users.push(userId);
}, },
); );
const diff = []; const diffs = [];
// XXX: This is also surely broken for i18n
users.forEach((userId) => { users.forEach((userId) => {
// Previous power level // Previous power level
const from = event.getPrevContent().users[userId]; const from = event.getPrevContent().users[userId];
// Current power level // Current power level
const to = event.getContent().users[userId]; const to = event.getContent().users[userId];
if (to !== from) { if (to !== from) {
diff.push( diffs.push({ userId, from, to });
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId,
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
}),
);
} }
}); });
if (!diff.length) { if (!diffs.length) {
return ''; return null;
} }
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { // XXX: This is also surely broken for i18n
return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName, senderName,
powerLevelDiffText: diff.join(", "), powerLevelDiffText: diffs.map(diff =>
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: diff.userId,
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
}),
).join(", "),
}); });
} }
function textForPinnedEvent(event) { function textForPinnedEvent(event): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
return _t("%(senderName)s changed the pinned messages for the room.", {senderName}); return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
} }
function textForWidgetEvent(event) { function textForWidgetEvent(event): () => string | null {
const senderName = event.getSender(); const senderName = event.getSender();
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
const {name, type, url} = event.getContent() || {}; const {name, type, url} = event.getContent() || {};
@ -464,27 +486,27 @@ function textForWidgetEvent(event) {
// equivalent to that condition. // equivalent to that condition.
if (url) { if (url) {
if (prevUrl) { if (prevUrl) {
return _t('%(widgetName)s widget modified by %(senderName)s', { return () => _t('%(widgetName)s widget modified by %(senderName)s', {
widgetName, senderName, widgetName, senderName,
}); });
} else { } else {
return _t('%(widgetName)s widget added by %(senderName)s', { return () => _t('%(widgetName)s widget added by %(senderName)s', {
widgetName, senderName, widgetName, senderName,
}); });
} }
} else { } else {
return _t('%(widgetName)s widget removed by %(senderName)s', { return () => _t('%(widgetName)s widget removed by %(senderName)s', {
widgetName, senderName, widgetName, senderName,
}); });
} }
} }
function textForWidgetLayoutEvent(event) { function textForWidgetLayoutEvent(event): () => string | null {
const senderName = event.sender?.name || event.getSender(); const senderName = event.sender?.name || event.getSender();
return _t("%(senderName)s has updated the widget layout", {senderName}); return () => _t("%(senderName)s has updated the widget layout", {senderName});
} }
function textForMjolnirEvent(event) { function textForMjolnirEvent(event): () => string | null {
const senderName = event.getSender(); const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent(); const {entity: prevEntity} = event.getPrevContent();
const {entity, recommendation, reason} = event.getContent(); const {entity, recommendation, reason} = event.getContent();
@ -492,74 +514,74 @@ function textForMjolnirEvent(event) {
// Rule removed // Rule removed
if (!entity) { if (!entity) {
if (USER_RULE_TYPES.includes(event.getType())) { if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning users matching %(glob)s", return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
{senderName, glob: prevEntity}); {senderName, glob: prevEntity});
} else if (ROOM_RULE_TYPES.includes(event.getType())) { } else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning rooms matching %(glob)s", return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
{senderName, glob: prevEntity}); {senderName, glob: prevEntity});
} else if (SERVER_RULE_TYPES.includes(event.getType())) { } else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s removed the rule banning servers matching %(glob)s", return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
{senderName, glob: prevEntity}); {senderName, glob: prevEntity});
} }
// Unknown type. We'll say something, but we shouldn't end up here. // Unknown type. We'll say something, but we shouldn't end up here.
return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity}); return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
} }
// Invalid rule // Invalid rule
if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName}); if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName});
// Rule updated // Rule updated
if (entity === prevEntity) { if (entity === prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) { if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s", return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) { } else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s", return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) { } else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s", return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} }
// Unknown type. We'll say something but we shouldn't end up here. // Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s", return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} }
// New rule // New rule
if (!prevEntity) { if (!prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) { if (USER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s", return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) { } else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s", return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) { } else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s", return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} }
// Unknown type. We'll say something but we shouldn't end up here. // Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s", return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason}); {senderName, glob: entity, reason});
} }
// else the entity !== prevEntity - count as a removal & add // else the entity !== prevEntity - count as a removal & add
if (USER_RULE_TYPES.includes(event.getType())) { if (USER_RULE_TYPES.includes(event.getType())) {
return _t( return () => _t(
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s", "%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason}, {senderName, oldGlob: prevEntity, newGlob: entity, reason},
); );
} else if (ROOM_RULE_TYPES.includes(event.getType())) { } else if (ROOM_RULE_TYPES.includes(event.getType())) {
return _t( return () => _t(
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s", "%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason}, {senderName, oldGlob: prevEntity, newGlob: entity, reason},
); );
} else if (SERVER_RULE_TYPES.includes(event.getType())) { } else if (SERVER_RULE_TYPES.includes(event.getType())) {
return _t( return () => _t(
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s", "%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason}, {senderName, oldGlob: prevEntity, newGlob: entity, reason},
@ -567,11 +589,15 @@ function textForMjolnirEvent(event) {
} }
// Unknown type. We'll say something but we shouldn't end up here. // Unknown type. We'll say something but we shouldn't end up here.
return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " + return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason}); "for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
} }
const handlers = { interface IHandlers {
[type: string]: (ev: any) => (() => string | null);
}
const handlers: IHandlers = {
'm.room.message': textForMessageEvent, 'm.room.message': textForMessageEvent,
'm.call.invite': textForCallInviteEvent, 'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent, 'm.call.answer': textForCallAnswerEvent,
@ -579,7 +605,7 @@ const handlers = {
'm.call.reject': textForCallRejectEvent, 'm.call.reject': textForCallRejectEvent,
}; };
const stateHandlers = { const stateHandlers: IHandlers = {
'm.room.canonical_alias': textForCanonicalAliasEvent, 'm.room.canonical_alias': textForCanonicalAliasEvent,
'm.room.name': textForRoomNameEvent, 'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent, 'm.room.topic': textForTopicEvent,
@ -604,8 +630,12 @@ for (const evType of ALL_RULE_TYPES) {
stateHandlers[evType] = textForMjolnirEvent; stateHandlers[evType] = textForMjolnirEvent;
} }
export function textForEvent(ev) { export function hasText(ev): boolean {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
if (handler) return handler(ev); return Boolean(handler?.(ev));
return ''; }
export function textForEvent(ev): string {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
return handler?.(ev)?.() || '';
} }

View file

@ -24,7 +24,9 @@ import { Room } from 'matrix-js-sdk/src/models/room';
// is sip virtual: there could be others in the future. // is sip virtual: there could be others in the future.
export default class VoipUserMapper { export default class VoipUserMapper {
private virtualRoomIdCache = new Set<string>(); // We store mappings of virtual -> native room IDs here until the local echo for the
// account data arrives.
private virtualToNativeRoomIdCache = new Map<string, string>();
public static sharedInstance(): VoipUserMapper { public static sharedInstance(): VoipUserMapper {
if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper();
@ -49,10 +51,20 @@ export default class VoipUserMapper {
native_room: roomId, native_room: roomId,
}); });
this.virtualToNativeRoomIdCache.set(virtualRoomId, roomId);
return virtualRoomId; return virtualRoomId;
} }
public nativeRoomForVirtualRoom(roomId: string): string { public nativeRoomForVirtualRoom(roomId: string): string {
const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
if (cachedNativeRoomId) {
console.log(
"Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache",
);
return cachedNativeRoomId;
}
const virtualRoom = MatrixClientPeg.get().getRoom(roomId); const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
if (!virtualRoom) return null; if (!virtualRoom) return null;
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
@ -67,7 +79,7 @@ export default class VoipUserMapper {
public isVirtualRoom(room: Room): boolean { public isVirtualRoom(room: Room): boolean {
if (this.nativeRoomForVirtualRoom(room.roomId)) return true; if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
if (this.virtualRoomIdCache.has(room.roomId)) return true; if (this.virtualToNativeRoomIdCache.has(room.roomId)) return true;
// also look in the create event for the claimed native room ID, which is the only // also look in the create event for the claimed native room ID, which is the only
// way we can recognise a virtual room we've created when it first arrives down // way we can recognise a virtual room we've created when it first arrives down
@ -110,7 +122,7 @@ export default class VoipUserMapper {
// also put this room in the virtual room ID cache so isVirtualRoom return the right answer // also put this room in the virtual room ID cache so isVirtualRoom return the right answer
// in however long it takes for the echo of setAccountData to come down the sync // in however long it takes for the echo of setAccountData to come down the sync
this.virtualRoomIdCache.add(invitedRoom.roomId); this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId);
} }
} }
} }

View file

@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {} interface IProps {
onClick?(): void;
}
interface IState {} interface IState {}
@replaceableComponent("structures.HostSignupAction") @replaceableComponent("structures.HostSignupAction")
export default class HostSignupAction extends React.PureComponent<IProps, IState> { export default class HostSignupAction extends React.PureComponent<IProps, IState> {
private openDialog = async () => { private openDialog = async () => {
this.props.onClick?.();
await HostSignupStore.instance.setHostSignupActive(true); await HostSignupStore.instance.setHostSignupActive(true);
} }

View file

@ -439,6 +439,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onBlur={this.onBlur} onBlur={this.onBlur}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
activeSpace={this.state.activeSpace} activeSpace={this.state.activeSpace}
onResize={this.refreshStickyHeaders}
onListCollapse={this.refreshStickyHeaders} onListCollapse={this.refreshStickyHeaders}
/>; />;

View file

@ -1953,6 +1953,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// Create and start the client // Create and start the client
await Lifecycle.setLoggedIn(credentials); await Lifecycle.setLoggedIn(credentials);
await this.postLoginSetup(); await this.postLoginSetup();
PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN); PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER); PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
}; };

View file

@ -25,10 +25,11 @@ import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore'; import SettingsStore from '../../settings/SettingsStore';
import RoomContext from "../../contexts/RoomContext";
import {Layout, LayoutPropType} from "../../settings/Layout"; import {Layout, LayoutPropType} from "../../settings/Layout";
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent"; import {hasText} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap"; import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro"; import NewRoomIntro from "../views/rooms/NewRoomIntro";
@ -151,6 +152,8 @@ export default class MessagePanel extends React.Component {
enableFlair: PropTypes.bool, enableFlair: PropTypes.bool,
}; };
static contextType = RoomContext;
constructor(props) { constructor(props) {
super(props); super(props);
@ -380,7 +383,7 @@ export default class MessagePanel extends React.Component {
// Always show highlighted event // Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true; if (this.props.highlightedEventId === mxEv.getId()) return true;
return !shouldHideEvent(mxEv); return !shouldHideEvent(mxEv, this.context);
} }
_readMarkerForEvent(eventId, isLastEvent) { _readMarkerForEvent(eventId, isLastEvent) {
@ -1164,11 +1167,8 @@ class MemberGrouper {
add(ev) { add(ev) {
if (ev.getType() === 'm.room.member') { if (ev.getType() === 'm.room.member') {
// We'll just double check that it's worth our time to do so, through an // We can ignore any events that don't actually have a message to display
// ugly hack. If textForEvent returns something, we should group it for if (!hasText(ev)) return;
// rendering but if it doesn't then we'll exclude it.
const renderText = textForEvent(ev);
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
} }
this.readMarker = this.readMarker || this.panel._readMarkerForEvent( this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
ev.getId(), ev.getId(),

View file

@ -41,7 +41,7 @@ export function getUnsentMessages(room) {
} }
@replaceableComponent("structures.RoomStatusBar") @replaceableComponent("structures.RoomStatusBar")
export default class RoomStatusBar extends React.Component { export default class RoomStatusBar extends React.PureComponent {
static propTypes = { static propTypes = {
// the room this statusbar is representing. // the room this statusbar is representing.
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,

View file

@ -59,7 +59,6 @@ import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary"; import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
import ForwardMessage from "../views/rooms/ForwardMessage";
import SearchBar from "../views/rooms/SearchBar"; import SearchBar from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel"; import AuxPanel from "../views/rooms/AuxPanel";
@ -81,7 +80,6 @@ import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView"; import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import { omit } from 'lodash';
import UIStore from "../../stores/UIStore"; import UIStore from "../../stores/UIStore";
const DEBUG = false; const DEBUG = false;
@ -136,7 +134,6 @@ export interface IState {
// Whether to highlight the event scrolled to // Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean; isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent;
numUnreadMessages: number; numUnreadMessages: number;
draggingFile: boolean; draggingFile: boolean;
searching: boolean; searching: boolean;
@ -155,7 +152,6 @@ export interface IState {
canPeek: boolean; canPeek: boolean;
showApps: boolean; showApps: boolean;
isPeeking: boolean; isPeeking: boolean;
showReadReceipts: boolean;
showRightPanel: boolean; showRightPanel: boolean;
// error object, as from the matrix client/server API // error object, as from the matrix client/server API
// If we failed to load information about the room, // If we failed to load information about the room,
@ -183,6 +179,12 @@ export interface IState {
canReact: boolean; canReact: boolean;
canReply: boolean; canReply: boolean;
layout: Layout; layout: Layout;
lowBandwidth: boolean;
showReadReceipts: boolean;
showRedactions: boolean;
showJoinLeaves: boolean;
showAvatarChanges: boolean;
showDisplaynameChanges: boolean;
matrixClientIsReady: boolean; matrixClientIsReady: boolean;
showUrlPreview?: boolean; showUrlPreview?: boolean;
e2eStatus?: E2EStatus; e2eStatus?: E2EStatus;
@ -200,8 +202,7 @@ export default class RoomView extends React.Component<IProps, IState> {
private readonly dispatcherRef: string; private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription; private readonly roomStoreToken: EventSubscription;
private readonly rightPanelStoreToken: EventSubscription; private readonly rightPanelStoreToken: EventSubscription;
private readonly showReadReceiptsWatchRef: string; private settingWatchers: string[];
private readonly layoutWatcherRef: string;
private unmounted = false; private unmounted = false;
private permalinkCreators: Record<string, RoomPermalinkCreator> = {}; private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
@ -232,7 +233,6 @@ export default class RoomView extends React.Component<IProps, IState> {
canPeek: false, canPeek: false,
showApps: false, showApps: false,
isPeeking: false, isPeeking: false,
showReadReceipts: true,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
joining: false, joining: false,
atEndOfLiveTimeline: true, atEndOfLiveTimeline: true,
@ -242,6 +242,12 @@ export default class RoomView extends React.Component<IProps, IState> {
canReact: false, canReact: false,
canReply: false, canReply: false,
layout: SettingsStore.getValue("layout"), layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
dragCounter: 0, dragCounter: 0,
}; };
@ -268,9 +274,14 @@ export default class RoomView extends React.Component<IProps, IState> {
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate); WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null, this.settingWatchers = [
this.onReadReceiptsChange); SettingsStore.watchSetting("layout", null, () =>
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange); this.setState({ layout: SettingsStore.getValue("layout") }),
),
SettingsStore.watchSetting("lowBandwidth", null, () =>
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
),
];
} }
private onWidgetStoreUpdate = () => { private onWidgetStoreUpdate = () => {
@ -323,13 +334,45 @@ export default class RoomView extends React.Component<IProps, IState> {
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(), replyToEvent: RoomViewStore.getQuotingEvent(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client // we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(), wasContextSwitch: RoomViewStore.getWasContextSwitch(),
}; };
// Add watchers for each of the settings we just looked up
this.settingWatchers = this.settingWatchers.concat([
SettingsStore.watchSetting("showReadReceipts", null, () =>
this.setState({
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
}),
),
SettingsStore.watchSetting("showRedactions", null, () =>
this.setState({
showRedactions: SettingsStore.getValue("showRedactions", roomId),
}),
),
SettingsStore.watchSetting("showJoinLeaves", null, () =>
this.setState({
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
}),
),
SettingsStore.watchSetting("showAvatarChanges", null, () =>
this.setState({
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
}),
),
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
this.setState({
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
}),
),
]);
if (!initial && this.state.shouldPeek && !newState.shouldPeek) { if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now // Stop peeking because we have joined this room now
this.context.stopPeeking(); this.context.stopPeeking();
@ -528,16 +571,12 @@ export default class RoomView extends React.Component<IProps, IState> {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const hasPropsDiff = objectHasDiff(this.props, nextProps); const hasPropsDiff = objectHasDiff(this.props, nextProps);
// React only shallow comparison and we only want to trigger const { upgradeRecommendation, ...state } = this.state;
// a component re-render if a room requires an upgrade const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState;
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
const state = omit(this.state, ['upgradeRecommendation']);
const newState = omit(nextState, ['upgradeRecommendation'])
const hasStateDiff = const hasStateDiff =
objectHasDiff(state, newState) || newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade ||
(newUpgradeRecommendation.needsUpgrade === true) objectHasDiff(state, newState);
return hasPropsDiff || hasStateDiff; return hasPropsDiff || hasStateDiff;
} }
@ -638,10 +677,6 @@ export default class RoomView extends React.Component<IProps, IState> {
); );
} }
if (this.showReadReceiptsWatchRef) {
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
}
// cancel any pending calls to the rate_limited_funcs // cancel any pending calls to the rate_limited_funcs
this.updateRoomMembers.cancelPendingCall(); this.updateRoomMembers.cancelPendingCall();
@ -649,7 +684,9 @@ export default class RoomView extends React.Component<IProps, IState> {
// console.log("Tinter.tint from RoomView.unmount"); // console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme // Tinter.tint(); // reset colourscheme
SettingsStore.unwatchSetting(this.layoutWatcherRef); for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
}
} }
private onUserScroll = () => { private onUserScroll = () => {
@ -659,16 +696,11 @@ export default class RoomView extends React.Component<IProps, IState> {
room_id: this.state.room.roomId, room_id: this.state.room.roomId,
event_id: this.state.initialEventId, event_id: this.state.initialEventId,
highlighted: false, highlighted: false,
replyingToEvent: this.state.replyToEvent,
}); });
} }
} }
private onLayoutChange = () => {
this.setState({
layout: SettingsStore.getValue("layout"),
});
};
private onRightPanelStoreUpdate = () => { private onRightPanelStoreUpdate = () => {
this.setState({ this.setState({
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
@ -819,7 +851,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// update unread count when scrolled up // update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change // no change
} else if (!shouldHideEvent(ev)) { } else if (!shouldHideEvent(ev, this.state)) {
this.setState((state, props) => { this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1}; return {numUnreadMessages: state.numUnreadMessages + 1};
}); });
@ -1410,18 +1442,6 @@ export default class RoomView extends React.Component<IProps, IState> {
dis.dispatch({ action: "open_room_settings" }); dis.dispatch({ action: "open_room_settings" });
}; };
private onCancelClick = () => {
console.log("updateTint from onCancelClick");
this.updateTint();
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.fire(Action.FocusComposer);
};
private onAppsClick = () => { private onAppsClick = () => {
dis.dispatch({ dis.dispatch({
action: "appsDrawer", action: "appsDrawer",
@ -1614,29 +1634,45 @@ export default class RoomView extends React.Component<IProps, IState> {
let auxPanelMaxHeight = UIStore.instance.windowHeight - let auxPanelMaxHeight = UIStore.instance.windowHeight -
(54 + // height of RoomHeader (54 + // height of RoomHeader
36 + // height of the status area 36 + // height of the status area
51 + // minimum height of the message compmoser 51 + // minimum height of the message composer
120); // amount of desired scrollback 120); // amount of desired scrollback
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
// but it's better than the video going missing entirely // but it's better than the video going missing entirely
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50; if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
this.setState({auxPanelMaxHeight: auxPanelMaxHeight}); if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
this.setState({ auxPanelMaxHeight });
}
}; };
private onStatusBarVisible = () => { private onStatusBarVisible = () => {
if (this.unmounted) return; if (this.unmounted || this.state.statusBarVisible) return;
this.setState({ this.setState({ statusBarVisible: true });
statusBarVisible: true,
});
}; };
private onStatusBarHidden = () => { private onStatusBarHidden = () => {
// This is currently not desired as it is annoying if it keeps expanding and collapsing // This is currently not desired as it is annoying if it keeps expanding and collapsing
if (this.unmounted) return; if (this.unmounted || !this.state.statusBarVisible) return;
this.setState({ this.setState({ statusBarVisible: false });
statusBarVisible: false, };
});
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
* We pass it down to the scroll panel.
*/
private handleScrollKey = ev => {
let panel;
if (this.searchResultsPanel.current) {
panel = this.searchResultsPanel.current;
} else if (this.messagePanel) {
panel = this.messagePanel;
}
if (panel) {
panel.handleScrollKey(ev);
}
}; };
/** /**
@ -1837,11 +1873,7 @@ export default class RoomView extends React.Component<IProps, IState> {
let aux = null; let aux = null;
let previewBar; let previewBar;
let hideCancel = false; if (this.state.searching) {
if (this.state.forwardingEvent) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar aux = <SearchBar
searchInProgress={this.state.searchInProgress} searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick} onCancelClick={this.onCancelSearchClick}
@ -1850,7 +1882,6 @@ export default class RoomView extends React.Component<IProps, IState> {
/>; />;
} else if (showRoomUpgradeBar) { } else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />; aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
} else if (myMembership !== "join") { } else if (myMembership !== "join") {
// We do have a room object for this room, but we're not currently in it. // We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it. // We may have a 3rd party invite to it.
@ -1859,7 +1890,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inviterName = this.props.oobData.inviterName; inviterName = this.props.oobData.inviterName;
} }
const invitedEmail = this.props.threepidInvite?.toEmail; const invitedEmail = this.props.threepidInvite?.toEmail;
hideCancel = true;
previewBar = ( previewBar = (
<RoomPreviewBar <RoomPreviewBar
onJoinClick={this.onJoinButtonClicked} onJoinClick={this.onJoinButtonClicked}
@ -1977,11 +2007,8 @@ export default class RoomView extends React.Component<IProps, IState> {
hideMessagePanel = true; hideMessagePanel = true;
} }
const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null; let highlightedEventId = null;
if (this.state.forwardingEvent) { if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.forwardingEvent.getId();
} else if (shouldHighlight) {
highlightedEventId = this.state.initialEventId; highlightedEventId = this.state.initialEventId;
} }
@ -2070,7 +2097,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inRoom={myMembership === 'join'} inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}

View file

@ -520,6 +520,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
setError("Failed to update some suggestions. Try again later"); setError("Failed to update some suggestions. Try again later");
} }
setSaving(false); setSaving(false);
setSelected(new Map());
}} }}
kind="primary_outline" kind="primary_outline"
disabled={disabled} disabled={disabled}

View file

@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic";
import InlineSpinner from "../views/elements/InlineSpinner"; import InlineSpinner from "../views/elements/InlineSpinner";
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite"; import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
import {useRoomMembers} from "../../hooks/useRoomMembers"; import {useRoomMembers} from "../../hooks/useRoomMembers";
import createRoom, {IOpts, Preset} from "../../createRoom"; import createRoom, {IOpts} from "../../createRoom";
import Field from "../views/elements/Field"; import Field from "../views/elements/Field";
import {useEventEmitter} from "../../hooks/useEventEmitter"; import {useEventEmitter} from "../../hooks/useEventEmitter";
import withValidation from "../views/elements/Validation"; import withValidation from "../views/elements/Validation";
@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal"; import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import { Preset } from "matrix-js-sdk/src/@types/partials";
interface IProps { interface IProps {
space: Room; space: Room;

View file

@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window"; import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg"; import {MatrixClientPeg} from "../../MatrixClientPeg";
import RoomContext from "../../contexts/RoomContext";
import UserActivity from "../../UserActivity"; import UserActivity from "../../UserActivity";
import Modal from "../../Modal"; import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
@ -125,6 +126,8 @@ class TimelinePanel extends React.Component {
alwaysShowTimestamps: PropTypes.bool, alwaysShowTimestamps: PropTypes.bool,
} }
static contextType = RoomContext;
// a map from room id to read marker event timestamp // a map from room id to read marker event timestamp
static roomReadMarkerTsMap = {}; static roomReadMarkerTsMap = {};
@ -1288,7 +1291,7 @@ class TimelinePanel extends React.Component {
const shouldIgnore = !!ev.status || // local echo const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message (ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev); const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) { if (isWithoutTile || !node) {
// don't start counting if the event should be ignored, // don't start counting if the event should be ignored,

View file

@ -366,9 +366,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
const mxDomain = MatrixClientPeg.get().getDomain(); const mxDomain = MatrixClientPeg.get().getDomain();
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`))); const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
if (!hostSignupConfig.domains || validDomains.length > 0) { if (!hostSignupConfig.domains || validDomains.length > 0) {
topSection = <div onClick={this.onCloseMenu}> topSection = <HostSignupAction onClick={this.onCloseMenu} />;
<HostSignupAction />
</div>;
} }
} }
} }

View file

@ -22,6 +22,7 @@ import classNames from 'classnames';
import * as AvatarLogic from '../../../Avatar'; import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units"; import {toPx} from "../../../utils/units";
@ -44,12 +45,12 @@ interface IProps {
className?: string; className?: string;
} }
const calculateUrls = (url, urls) => { const calculateUrls = (url, urls, lowBandwidth) => {
// work out the full set of urls to try to load. This is formed like so: // work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ] // imageUrls: [ props.url, ...props.urls ]
let _urls = []; let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) { if (!lowBandwidth) {
_urls = urls || []; _urls = urls || [];
if (url) { if (url) {
@ -63,7 +64,13 @@ const calculateUrls = (url, urls) => {
}; };
const useImageUrl = ({url, urls}): [string, () => void] => { const useImageUrl = ({url, urls}): [string, () => void] => {
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls)); // Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext);
const lowBandwidth = roomContext ?
roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState<number>(0); const [urlsIndex, setIndex] = useState<number>(0);
const onError = useCallback(() => { const onError = useCallback(() => {
@ -71,7 +78,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => {
}, []); }, []);
useEffect(() => { useEffect(() => {
setUrls(calculateUrls(url, urls)); setUrls(calculateUrls(url, urls, lowBandwidth));
setIndex(0); setIndex(0);
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps

View file

@ -32,6 +32,7 @@ import { MenuItem } from "../../structures/ContextMenu";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
export function canCancel(eventStatus) { export function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -157,10 +158,10 @@ export default class MessageContextMenu extends React.Component {
}; };
onForwardClick = () => { onForwardClick = () => {
if (this.props.onCloseDialog) this.props.onCloseDialog(); Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
dis.dispatch({ matrixClient: MatrixClientPeg.get(),
action: 'forward_event',
event: this.props.mxEvent, event: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
}); });
this.closeMenu(); this.closeMenu();
}; };

View file

@ -23,7 +23,7 @@ import withValidation, {IFieldState} from '../elements/Validation';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom"; import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field from "../elements/Field"; import Field from "../elements/Field";
@ -31,6 +31,7 @@ import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog"; import BaseDialog from "../dialogs/BaseDialog";
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
interface IProps { interface IProps {
defaultPublic?: boolean; defaultPublic?: boolean;
@ -72,7 +73,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
canChangeEncryption: true, canChangeEncryption: true,
}; };
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private") MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat)
.then(isForced => this.setState({ canChangeEncryption: !isForced })); .then(isForced => this.setState({ canChangeEncryption: !isForced }));
} }

View file

@ -0,0 +1,247 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useMemo, useState, useEffect} from "react";
import classnames from "classnames";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {_t} from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
import {UIFeature} from "../../../settings/UIFeature";
import {Layout} from "../../../settings/Layout";
import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import {avatarUrlForUser} from "../../../Avatar";
import EventTile from "../rooms/EventTile";
import SearchBox from "../../structures/SearchBox";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {Alignment} from '../elements/Tooltip';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "../rooms/NotificationBadge";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
const AVATAR_SIZE = 30;
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
// The event to forward
event: MatrixEvent;
// We need a permalink creator for the source room to pass through to EventTile
// in case the event is a reply (even though the user can't get at the link)
permalinkCreator: RoomPermalinkCreator;
}
interface IEntryProps {
room: Room;
event: MatrixEvent;
matrixClient: MatrixClient;
onFinished(success: boolean): void;
}
enum SendState {
CanSend,
Sending,
Sent,
Failed,
}
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
const jumpToRoom = () => {
dis.dispatch({
action: "view_room",
room_id: room.roomId,
});
onFinished(true);
};
const send = async () => {
setSendState(SendState.Sending);
try {
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
setSendState(SendState.Sent);
} catch (e) {
setSendState(SendState.Failed);
}
};
let className;
let disabled = false;
let title;
let icon;
if (sendState === SendState.CanSend) {
className = "mx_ForwardList_canSend";
if (room.maySendMessage()) {
title = _t("Send");
} else {
disabled = true;
title = _t("You don't have permission to do this");
}
} else if (sendState === SendState.Sending) {
className = "mx_ForwardList_sending";
disabled = true;
title = _t("Sending");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
} else if (sendState === SendState.Sent) {
className = "mx_ForwardList_sent";
disabled = true;
title = _t("Sent");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
} else {
className = "mx_ForwardList_sendFailed";
disabled = true;
title = _t("Failed to send");
icon = <NotificationBadge
notification={StaticNotificationState.RED_EXCLAMATION}
/>;
}
return <div className="mx_ForwardList_entry">
<AccessibleTooltipButton
className="mx_ForwardList_roomButton"
onClick={jumpToRoom}
title={_t("Open link")}
yOffset={-20}
alignment={Alignment.Top}
>
<DecoratedRoomAvatar room={room} avatarSize={32} />
<span className="mx_ForwardList_entry_name">{ room.name }</span>
</AccessibleTooltipButton>
<AccessibleTooltipButton
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
className={`mx_ForwardList_sendButton ${className}`}
onClick={send}
disabled={disabled}
title={title}
yOffset={-20}
alignment={Alignment.Top}
>
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
{ icon }
</AccessibleTooltipButton>
</div>;
};
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
const userId = cli.getUserId();
const [profileInfo, setProfileInfo] = useState<any>({});
useEffect(() => {
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
}, [cli, userId]);
// For the message preview we fake the sender as ourselves
const mockEvent = new MatrixEvent({
type: "m.room.message",
sender: userId,
content: event.getContent(),
unsigned: {
age: 97,
},
event_id: "$9999999999999999999999999999999999999999999",
room_id: event.getRoomId(),
});
mockEvent.sender = {
name: profileInfo.displayname || userId,
userId,
getAvatarUrl: (..._) => {
return avatarUrlForUser(
{ avatarUrl: profileInfo.avatar_url },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => profileInfo.avatar_url,
};
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
const spacesEnabled = useFeatureEnabled("feature_spaces");
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
const previewLayout = useSettingValue<Layout>("layout");
let rooms = useMemo(() => sortRooms(
cli.getVisibleRooms().filter(
room => room.getMyMembership() === "join" &&
!(spacesEnabled && room.isSpaceRoom()),
),
), [cli, spacesEnabled]);
if (lcQuery) {
rooms = new QueryMatcher<Room>(rooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
}).match(lcQuery);
}
return <BaseDialog
title={_t("Forward message")}
className="mx_ForwardDialog"
contentId="mx_ForwardList"
onFinished={onFinished}
fixedWidth={false}
>
<h3>{ _t("Message preview") }</h3>
<div className={classnames("mx_ForwardDialog_preview", {
"mx_IRCLayout": previewLayout == Layout.IRC,
"mx_GroupLayout": previewLayout == Layout.Group,
})}>
<EventTile
mxEvent={mockEvent}
layout={previewLayout}
enableFlair={flairEnabled}
permalinkCreator={permalinkCreator}
as="div"
/>
</div>
<hr />
<div className="mx_ForwardList" id="mx_ForwardList">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search for rooms or people")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ForwardList_content">
{ rooms.length > 0 ? (
<div className="mx_ForwardList_results">
{ rooms.map(room =>
<Entry
key={room.roomId}
room={room}
event={event}
matrixClient={cli}
onFinished={onFinished}
/>,
) }
</div>
) : <span className="mx_ForwardList_noResults">
{ _t("No results") }
</span> }
</AutoHideScrollbar>
</div>
</BaseDialog>;
};
export default ForwardDialog;

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import classNames from 'classnames';
import {_t, _td} from "../../../languageHandler"; import {_t, _td} from "../../../languageHandler";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
@ -31,7 +33,6 @@ import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize"; import {humanizeTime} from "../../../utils/humanize";
import createRoom, { import createRoom, {
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted, canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
IInvite3PID,
} from "../../../createRoom"; } from "../../../createRoom";
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
@ -50,6 +51,12 @@ import {getAddressType} from "../../../UserAddress";
import BaseAvatar from '../avatars/BaseAvatar'; import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { compare } from '../../../utils/strings'; import { compare } from '../../../utils/strings';
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { copyPlaintext, selectText } from "../../../utils/strings";
import * as ContextMenu from "../../structures/ContextMenu";
import { toRightOf } from "../../structures/ContextMenu";
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -351,6 +358,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
initialText: "", initialText: "",
}; };
private closeCopiedTooltip: () => void;
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
private editorRef = createRef<HTMLInputElement>(); private editorRef = createRef<HTMLInputElement>();
private unmounted = false; private unmounted = false;
@ -403,6 +411,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
componentWillUnmount() { componentWillUnmount() {
this.unmounted = true; this.unmounted = true;
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
} }
private onConsultFirstChange = (ev) => { private onConsultFirstChange = (ev) => {
@ -1238,6 +1249,25 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
} }
} }
private async onLinkClick(e) {
e.preventDefault();
selectText(e.target);
}
private onCopyClick = async e => {
e.preventDefault();
const target = e.target; // copy target before we go async and React throws it away
const successful = await copyPlaintext(makeUserPermalink(MatrixClientPeg.get().getUserId()));
const buttonRect = target.getBoundingClientRect();
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t("Copied!") : _t("Failed to copy"),
});
// Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = target.onmouseleave = close;
};
render() { render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@ -1248,12 +1278,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
spinner = <Spinner w={20} h={20} />; spinner = <Spinner w={20} h={20} />;
} }
let title; let title;
let helpText; let helpText;
let buttonText; let buttonText;
let goButtonFn; let goButtonFn;
let consultSection; let extraSection;
let footer;
let keySharingWarning = <span />; let keySharingWarning = <span />;
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
@ -1316,6 +1346,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
} }
buttonText = _t("Go"); buttonText = _t("Go");
goButtonFn = this.startDm; goButtonFn = this.startDm;
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
<span>{ _t("Some suggestions may be hidden for privacy.") }</span>
<p>{ _t("If you can't see who youre looking for, send them your invite link below.") }</p>
</div>;
const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
footer = <div className="mx_InviteDialog_footer">
<h3>{ _t("Or send invite link") }</h3>
<div className="mx_InviteDialog_footer_link">
<a href={link} onClick={this.onLinkClick}>
{ link }
</a>
<AccessibleTooltipButton
title={_t("Copy")}
onClick={this.onCopyClick}
className="mx_InviteDialog_footer_link_copy"
>
<div />
</AccessibleTooltipButton>
</div>
</div>
} else if (this.props.kind === KIND_INVITE) { } else if (this.props.kind === KIND_INVITE) {
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId); const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom(); const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
@ -1377,7 +1427,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
title = _t("Transfer"); title = _t("Transfer");
buttonText = _t("Transfer"); buttonText = _t("Transfer");
goButtonFn = this.transferCall; goButtonFn = this.transferCall;
consultSection = <div> footer = <div>
<label> <label>
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} /> <input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
{_t("Consult first")} {_t("Consult first")}
@ -1391,7 +1441,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|| (this.state.filterText && this.state.filterText.includes('@')); || (this.state.filterText && this.state.filterText.includes('@'));
return ( return (
<BaseDialog <BaseDialog
className='mx_InviteDialog' className={classNames("mx_InviteDialog", {
mx_InviteDialog_hasFooter: !!footer,
})}
hasCancel={true} hasCancel={true}
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={title} title={title}
@ -1418,8 +1470,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
<div className='mx_InviteDialog_userSections'> <div className='mx_InviteDialog_userSections'>
{this.renderSection('recents')} {this.renderSection('recents')}
{this.renderSection('suggestions')} {this.renderSection('suggestions')}
{extraSection}
</div> </div>
{consultSection} {footer}
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -16,7 +16,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import TabbedView, {Tab} from "../../structures/TabbedView"; import TabbedView, {Tab} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler"; import {_t, _td} from "../../../languageHandler";
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab"; import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
@ -39,31 +38,36 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
interface IProps {
roomId: string;
onFinished: (success: boolean) => void;
initialTabId?: string;
}
@replaceableComponent("views.dialogs.RoomSettingsDialog") @replaceableComponent("views.dialogs.RoomSettingsDialog")
export default class RoomSettingsDialog extends React.Component { export default class RoomSettingsDialog extends React.Component<IProps> {
static propTypes = { private dispatcherRef: string;
roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
componentDidMount() { public componentDidMount() {
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
componentWillUnmount() { public componentWillUnmount() {
if (this._dispatcherRef) dis.unregister(this._dispatcherRef); if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
}
} }
_onAction = (payload) => { private onAction = (payload): void => {
// When view changes below us, close the room settings // When view changes below us, close the room settings
// whilst the modal is open this can only be triggered when someone hits Leave Room // whilst the modal is open this can only be triggered when someone hits Leave Room
if (payload.action === 'view_home_page') { if (payload.action === 'view_home_page') {
this.props.onFinished(); this.props.onFinished(true);
} }
}; };
_getTabs() { private getTabs(): Tab[] {
const tabs = []; const tabs: Tab[] = [];
tabs.push(new Tab( tabs.push(new Tab(
ROOM_GENERAL_TAB, ROOM_GENERAL_TAB,
@ -123,7 +127,10 @@ export default class RoomSettingsDialog extends React.Component {
title={_t("Room Settings - %(roomName)s", {roomName})} title={_t("Room Settings - %(roomName)s", {roomName})}
> >
<div className='mx_SettingsDialog_content'> <div className='mx_SettingsDialog_content'>
<TabbedView tabs={this._getTabs()} /> <TabbedView
tabs={this.getTabs()}
initialTabId={this.props.initialTabId}
/>
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -19,7 +19,7 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip'; import Tooltip, {Alignment} from './Tooltip';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> { interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
tooltipClassName?: string; tooltipClassName?: string;
forceHide?: boolean; forceHide?: boolean;
yOffset?: number; yOffset?: number;
alignment?: Alignment;
} }
interface IState { interface IState {
@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
render() { render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props; const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
const tip = this.state.hover ? <Tooltip const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container" className="mx_AccessibleTooltipButton_container"
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)} tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title} label={tooltip || title}
yOffset={yOffset} yOffset={yOffset}
alignment={alignment}
/> : null; /> : null;
return ( return (
<AccessibleButton <AccessibleButton

View file

@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
// The key used for PersistedElement // The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id); this._persistKey = getPersistKey(this.props.app.id);
try {
this._sgWidget = new StopGapWidget(this.props); this._sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared); this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this._sgWidget.on("ready", this._onWidgetReady);
} catch (e) {
console.log("Failed to construct widget", e);
this._sgWidget = null;
}
this.iframe = null; // ref to the iframe (callback style) this.iframe = null; // ref to the iframe (callback style)
this.state = this._getNewState(props); this.state = this._getNewState(props);
@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
// Force the widget to be non-persistent (able to be deleted/forgotten) // Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this._persistKey);
this._sgWidget.stop(); if (this._sgWidget) this._sgWidget.stop();
} }
this.setState({ hasPermissionToLoad }); this.setState({ hasPermissionToLoad });
@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
componentDidMount() { componentDidMount() {
// Only fetch IM token on mount if we're showing and have permission to load // Only fetch IM token on mount if we're showing and have permission to load
if (this.state.hasPermissionToLoad) { if (this._sgWidget && this.state.hasPermissionToLoad) {
this._startWidget(); this._startWidget();
} }
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
if (this._sgWidget) { if (this._sgWidget) {
this._sgWidget.stop(); this._sgWidget.stop();
} }
try {
this._sgWidget = new StopGapWidget(newProps); this._sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared); this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this._sgWidget.on("ready", this._onWidgetReady);
this._startWidget(); this._startWidget();
} catch (e) {
console.log("Failed to construct widget", e);
this._sgWidget = null;
}
} }
_startWidget() { _startWidget() {
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
_iframeRefChange = (ref) => { _iframeRefChange = (ref) => {
this.iframe = ref; this.iframe = ref;
if (ref) { if (ref) {
this._sgWidget.start(ref); if (this._sgWidget) this._sgWidget.start(ref);
} else { } else {
this._resetWidget(this.props); this._resetWidget(this.props);
} }
@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this._persistKey);
this._sgWidget.stop({forceDestroy: true}); if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
} }
_onWidgetPrepared = () => { _onWidgetPrepared = () => {
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
<Spinner message={_t("Loading...")} /> <Spinner message={_t("Loading...")} />
</div> </div>
); );
if (!this.state.hasPermissionToLoad) { if (this._sgWidget === null) {
appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg={_t("Error loading Widget")} />
</div>
);
} else if (!this.state.hasPermissionToLoad) {
// only possible for room widgets, can assert this.props.room here // only possible for room widgets, can assert this.props.room here
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = ( appTileBody = (
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
if (this.isMixedContent()) { if (this.isMixedContent()) {
appTileBody = ( appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}> <div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg="Error - Mixed content" /> <AppWarning errorMsg={_t("Error - Mixed content")} />
</div> </div>
); );
} else { } else {

View file

@ -297,6 +297,7 @@ export default class ReplyThread extends React.Component {
} }
async getEvent(eventId) { async getEvent(eventId) {
if (!eventId) return null;
const event = this.room.findEventById(eventId); const event = this.room.findEventById(eventId);
if (event) return event; if (event) return event;

View file

@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {forwardRef, ReactNode} from "react"; import React, {forwardRef, ReactNode, ReactChildren} from "react";
import classNames from "classnames"; import classNames from "classnames";
interface IProps { interface IProps {
className: string; className: string;
title: string; title: string;
subtitle?: ReactNode; subtitle?: ReactNode;
children?: ReactChildren;
} }
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => { const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => {

View file

@ -16,20 +16,19 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils'; import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.messages.MessageTimestamp") interface IProps {
export default class MessageTimestamp extends React.Component { ts: number;
static propTypes = { showTwelveHour?: boolean;
ts: PropTypes.number.isRequired, showFullDate?: boolean;
showTwelveHour: PropTypes.bool, showSeconds?: boolean;
showFullDate: PropTypes.bool, }
showSeconds: PropTypes.bool,
};
render() { @replaceableComponent("views.messages.MessageTimestamp")
export default class MessageTimestamp extends React.Component<IProps> {
public render() {
const date = new Date(this.props.ts); const date = new Date(this.props.ts);
let timestamp; let timestamp;
if (this.props.showFullDate) { if (this.props.showFullDate) {
@ -41,7 +40,11 @@ export default class MessageTimestamp extends React.Component {
} }
return ( return (
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}> <span
className="mx_MessageTimestamp"
title={formatFullDate(date, this.props.showTwelveHour)}
aria-hidden={true}
>
{timestamp} {timestamp}
</span> </span>
); );

View file

@ -15,24 +15,31 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import Flair from '../elements/Flair.js'; import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore'; import FlairStore from '../../../stores/FlairStore';
import {getUserNameColorClass} from '../../../utils/FormattingUtils'; import {getUserNameColorClass} from '../../../utils/FormattingUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import MatrixEvent from "matrix-js-sdk/src/models/event";
interface IProps {
mxEvent: MatrixEvent;
onClick(): void;
enableFlair: boolean;
}
interface IState {
userGroups;
relatedGroups;
}
@replaceableComponent("views.messages.SenderProfile") @replaceableComponent("views.messages.SenderProfile")
export default class SenderProfile extends React.Component { export default class SenderProfile extends React.Component<IProps, IState> {
static propTypes = {
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
onClick: PropTypes.func,
};
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
private unmounted: boolean;
constructor(props) { constructor(props: IProps) {
super(props); super(props)
const senderId = this.props.mxEvent.getSender(); const senderId = this.props.mxEvent.getSender();
this.state = { this.state = {
@ -40,6 +47,7 @@ export default class SenderProfile extends React.Component {
relatedGroups: [], relatedGroups: [],
}; };
} }
componentDidMount() { componentDidMount() {
this.unmounted = false; this.unmounted = false;
this._updateRelatedGroups(); this._updateRelatedGroups();
@ -100,14 +108,26 @@ export default class SenderProfile extends React.Component {
render() { render() {
const {mxEvent} = this.props; const {mxEvent} = this.props;
const colorClass = getUserNameColorClass(mxEvent.getSender()); const colorClass = getUserNameColorClass(mxEvent.getSender());
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
const {msgtype} = mxEvent.getContent(); const {msgtype} = mxEvent.getContent();
const disambiguate = mxEvent.sender?.disambiguate;
const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
if (msgtype === 'm.emote') { if (msgtype === 'm.emote') {
return null; // emote message must include the name so don't duplicate it return null; // emote message must include the name so don't duplicate it
} }
let flair = null; let mxidElement;
if (disambiguate) {
mxidElement = (
<span className="mx_SenderProfile_mxid">
{ mxid }
</span>
);
}
let flair;
if (this.props.enableFlair) { if (this.props.enableFlair) {
const displayedGroups = this._getDisplayedGroups( const displayedGroups = this._getDisplayedGroups(
this.state.userGroups, this.state.relatedGroups, this.state.userGroups, this.state.relatedGroups,
@ -119,13 +139,12 @@ export default class SenderProfile extends React.Component {
/>; />;
} }
const nameElem = name || '';
return ( return (
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}> <div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
<span className={`mx_SenderProfile_name ${colorClass}`}> <span className={`mx_SenderProfile_displayName ${colorClass}`}>
{ nameElem } { displayName }
</span> </span>
{ mxidElement }
{ flair } { flair }
</div> </div>
); );

View file

@ -155,12 +155,24 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
// show them in reverse, with latest pinned at the top // show them in reverse, with latest pinned at the top
content = pinnedEvents.filter(Boolean).reverse().map(ev => ( content = pinnedEvents.filter(Boolean).reverse().map(ev => (
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={onUnpinClicked} /> <PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
)); ));
} else { } else {
content = <div className="mx_RightPanel_empty mx_PinnedMessagesCard_empty"> content = <div className="mx_PinnedMessagesCard_empty">
<h2>{_t("Youre all caught up")}</h2> <div>
<p>{_t("You have no visible notifications.")}</p> { /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
<div className="mx_PinnedMessagesCard_MessageActionBar">
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" />
</div>
<h2>{ _t("Nothing pinned, yet") }</h2>
{ _t("If you have permissions, open the menu on any message and select " +
"<b>Pin</b> to stick them here.", {}, {
b: sub => <b>{ sub }</b>,
}) }
</div>
</div>; </div>;
} }

View file

@ -82,13 +82,6 @@ export default class AppsDrawer extends React.Component {
this.props.resizeNotifier.off("isResizing", this.onIsResizing); this.props.resizeNotifier.off("isResizing", this.onIsResizing);
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
// Room has changed probably, update apps
this._updateApps();
}
onIsResizing = (resizing) => { onIsResizing = (resizing) => {
// This one is the vertical, ie. change height of apps drawer // This one is the vertical, ie. change height of apps drawer
this.setState({ resizingVertical: resizing }); this.setState({ resizingVertical: resizing });
@ -141,7 +134,10 @@ export default class AppsDrawer extends React.Component {
_getAppsHash = (apps) => apps.map(app => app.id).join("~"); _getAppsHash = (apps) => apps.map(app => app.id).join("~");
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) { if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
// Room has changed, update apps
this._updateApps();
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
this._loadResizerPreferences(); this._loadResizerPreferences();
} }
} }

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Room } from 'matrix-js-sdk/src/models/room' import { Room } from 'matrix-js-sdk/src/models/room'
import dis from "../../../dispatcher/dispatcher";
import AppsDrawer from './AppsDrawer'; import AppsDrawer from './AppsDrawer';
import classNames from 'classnames'; import classNames from 'classnames';
import RateLimitedFunc from '../../../ratelimitedfunc'; import RateLimitedFunc from '../../../ratelimitedfunc';
@ -69,19 +68,21 @@ export default class AuxPanel extends React.Component<IProps, IState> {
super(props); super(props);
this.state = { this.state = {
counters: this._computeCounters(), counters: this.computeCounters(),
}; };
} }
componentDidMount() { componentDidMount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._rateLimitedUpdate); if (SettingsStore.getValue("feature_state_counters")) {
cli.on("RoomState.events", this.rateLimitedUpdate);
}
} }
componentWillUnmount() { componentWillUnmount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli && SettingsStore.getValue("feature_state_counters")) {
cli.removeListener("RoomState.events", this._rateLimitedUpdate); cli.removeListener("RoomState.events", this.rateLimitedUpdate);
} }
} }
@ -96,23 +97,11 @@ export default class AuxPanel extends React.Component<IProps, IState> {
} }
} }
onConferenceNotificationClick = (ev, type) => { private rateLimitedUpdate = new RateLimitedFunc(() => {
dis.dispatch({ this.setState({ counters: this.computeCounters() });
action: 'place_call',
type: type,
room_id: this.props.room.roomId,
});
ev.stopPropagation();
ev.preventDefault();
};
_rateLimitedUpdate = new RateLimitedFunc(() => {
if (SettingsStore.getValue("feature_state_counters")) {
this.setState({counters: this._computeCounters()});
}
}, 500); }, 500);
_computeCounters() { private computeCounters() {
const counters = []; const counters = [];
if (this.props.room && SettingsStore.getValue("feature_state_counters")) { if (this.props.room && SettingsStore.getValue("feature_state_counters")) {

View file

@ -25,7 +25,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import ReplyThread from "../elements/ReplyThread"; import ReplyThread from "../elements/ReplyThread";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as TextForEvent from "../../../TextForEvent"; import { hasText } from "../../../TextForEvent";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
@ -644,7 +644,18 @@ export default class EventTile extends React.Component<IProps, IState> {
// return early if there are no read receipts // return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) { if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return null; // We currently must include `mx_EventTile_readAvatars` in the DOM
// of all events, as it is the positioned parent of the animated
// read receipts. We can't let it unmount when a receipt moves
// events, so for now we mount it for all events. Without it, the
// animation will start from the top of the timeline (because it
// lost its container).
// See also https://github.com/vector-im/element-web/issues/17561
return (
<div className="mx_EventTile_msgOption">
<span className="mx_EventTile_readAvatars" />
</div>
);
} }
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
@ -652,11 +663,7 @@ export default class EventTile extends React.Component<IProps, IState> {
const receiptOffset = 15; const receiptOffset = 15;
let left = 0; let left = 0;
const receipts = this.props.readReceipts || []; const receipts = this.props.readReceipts;
if (receipts.length === 0) {
return null;
}
for (let i = 0; i < receipts.length; ++i) { for (let i = 0; i < receipts.length; ++i) {
const receipt = receipts[i]; const receipt = receipts[i];
@ -1052,22 +1059,26 @@ export default class EventTile extends React.Component<IProps, IState> {
switch (this.props.tileShape) { switch (this.props.tileShape) {
case 'notif': { case 'notif': {
const room = this.context.getRoom(this.props.mxEvent.getRoomId()); const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return ( return React.createElement(this.props.as || "li", {
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}> "className": classes,
<div className="mx_EventTile_roomName"> "aria-live": ariaLive,
"aria-atomic": true,
"data-scroll-tokens": scrollToken,
}, [
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} /> <RoomAvatar room={room} width={28} height={28} />
<a href={permalink} onClick={this.onPermalinkClicked}> <a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' } { room ? room.name : '' }
</a> </a>
</div> </div>,
<div className="mx_EventTile_senderDetails"> <div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
{ avatar } { avatar }
<a href={permalink} onClick={this.onPermalinkClicked}> <a href={permalink} onClick={this.onPermalinkClicked}>
{ sender } { sender }
{ timestamp } { timestamp }
</a> </a>
</div> </div>,
<div className="mx_EventTile_line"> <div className="mx_EventTile_line" key="mx_EventTile_line">
<EventTileType ref={this.tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
@ -1075,14 +1086,17 @@ export default class EventTile extends React.Component<IProps, IState> {
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}
/> />
</div> </div>,
</li> ]);
);
} }
case 'file_grid': { case 'file_grid': {
return ( return React.createElement(this.props.as || "li", {
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}> "className": classes,
<div className="mx_EventTile_line"> "aria-live": ariaLive,
"aria-atomic": true,
"data-scroll-tokens": scrollToken,
}, [
<div className="mx_EventTile_line" key="mx_EventTile_line">
<EventTileType ref={this.tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
@ -1091,9 +1105,10 @@ export default class EventTile extends React.Component<IProps, IState> {
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}
/> />
</div> </div>,
<a <a
className="mx_EventTile_senderDetailsLink" className="mx_EventTile_senderDetailsLink"
key="mx_EventTile_senderDetailsLink"
href={permalink} href={permalink}
onClick={this.onPermalinkClicked} onClick={this.onPermalinkClicked}
> >
@ -1101,9 +1116,8 @@ export default class EventTile extends React.Component<IProps, IState> {
{ sender } { sender }
{ timestamp } { timestamp }
</div> </div>
</a> </a>,
</li> ]);
);
} }
case 'reply': case 'reply':
@ -1119,13 +1133,17 @@ export default class EventTile extends React.Component<IProps, IState> {
this.props.alwaysShowTimestamps || this.state.hover, this.props.alwaysShowTimestamps || this.state.hover,
); );
} }
return ( return React.createElement(this.props.as || "li", {
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}> "className": classes,
{ ircTimestamp } "aria-live": ariaLive,
{ avatar } "aria-atomic": true,
{ sender } "data-scroll-tokens": scrollToken,
{ ircPadlock } }, [
<div className="mx_EventTile_reply"> ircTimestamp,
avatar,
sender,
ircPadlock,
<div className="mx_EventTile_reply" key="mx_EventTile_reply">
{ groupTimestamp } { groupTimestamp }
{ groupPadlock } { groupPadlock }
{ thread } { thread }
@ -1137,9 +1155,8 @@ export default class EventTile extends React.Component<IProps, IState> {
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
showUrlPreview={false} showUrlPreview={false}
/> />
</div> </div>,
</li> ]);
);
} }
default: { default: {
const thread = ReplyThread.makeThread( const thread = ReplyThread.makeThread(
@ -1210,7 +1227,7 @@ export function haveTileForEvent(e) {
const handler = getHandlerTile(e); const handler = getHandlerTile(e);
if (handler === undefined) return false; if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') { if (handler === 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== ''; return hasText(e);
} else if (handler === 'messages.RoomCreate') { } else if (handler === 'messages.RoomCreate') {
return Boolean(e.getContent()['predecessor']); return Boolean(e.getContent()['predecessor']);
} else { } else {

View file

@ -1,53 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 Michael Telatynski
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {Key} from '../../../Keyboard';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.ForwardMessage")
export default class ForwardMessage extends React.Component {
static propTypes = {
onCancelClick: PropTypes.func.isRequired,
};
componentDidMount() {
document.addEventListener('keydown', this._onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this._onKeyDown);
}
_onKeyDown = ev => {
switch (ev.key) {
case Key.ESCAPE:
this.props.onCancelClick();
break;
}
};
render() {
return (
<div className="mx_ForwardMessage">
<h1>{ _t('Please select the destination room for this message') }</h1>
</div>
);
}
}

View file

@ -31,6 +31,17 @@ import dis from "../../../dispatcher/dispatcher";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import {showSpaceInvite} from "../../../utils/space"; import {showSpaceInvite} from "../../../utils/space";
import { privateShouldBeEncrypted } from "../../../createRoom";
import EventTileBubble from "../messages/EventTileBubble";
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
function hasExpectedEncryptionSettings(room): boolean {
const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId);
const isPublic: boolean = room.getJoinRule() === "public";
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
}
const NewRoomIntro = () => { const NewRoomIntro = () => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const {room, roomId} = useContext(RoomContext); const {room, roomId} = useContext(RoomContext);
@ -166,7 +177,31 @@ const NewRoomIntro = () => {
</React.Fragment>; </React.Fragment>;
} }
function openRoomSettings(event) {
event.preventDefault();
dis.dispatch({
action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB,
});
}
const sub2 = _t(
"Your private messages are normally encrypted, but this room isn't. "+
"Usually this is due to an unsupported device or method being used, " +
"like email invites. <a>Enable encryption in settings.</a>", {},
{ a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> },
);
return <div className="mx_NewRoomIntro"> return <div className="mx_NewRoomIntro">
{ !hasExpectedEncryptionSettings(room) && (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
title={_t("End-to-end encryption isn't enabled")}
subtitle={sub2}
/>
)}
{ body } { body }
</div>; </div>;
}; };

View file

@ -95,6 +95,7 @@ export default class ReplyPreview extends React.Component {
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
enableFlair={SettingsStore.getValue(UIFeature.Flair)} enableFlair={SettingsStore.getValue(UIFeature.Flair)}
as="div"
/> />
</div> </div>
</div>; </div>;

View file

@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RateLimitedFunc from '../../../ratelimitedfunc'; import RateLimitedFunc from '../../../ratelimitedfunc';
import { CancelButton } from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
@ -42,7 +41,6 @@ export default class RoomHeader extends React.Component {
onSettingsClick: PropTypes.func, onSettingsClick: PropTypes.func,
onSearchClick: PropTypes.func, onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func, onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
e2eStatus: PropTypes.string, e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func, onAppsClick: PropTypes.func,
appsShown: PropTypes.bool, appsShown: PropTypes.bool,
@ -52,7 +50,6 @@ export default class RoomHeader extends React.Component {
static defaultProps = { static defaultProps = {
editing: false, editing: false,
inRoom: false, inRoom: false,
onCancelClick: null,
}; };
componentDidMount() { componentDidMount() {
@ -83,11 +80,6 @@ export default class RoomHeader extends React.Component {
render() { render() {
let searchStatus = null; let searchStatus = null;
let cancelButton = null;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
// don't display the search count until the search completes and // don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount. // gives us a valid (possibly zero) searchCount.
@ -207,7 +199,6 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div> <div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ name } { name }
{ topicElement } { topicElement }
{ cancelButton }
{ rightRow } { rightRow }
<RoomHeaderButtons room={this.props.room} /> <RoomHeaderButtons room={this.props.room} />
</div> </div>

View file

@ -55,6 +55,7 @@ interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void; onKeyDown: (ev: React.KeyboardEvent) => void;
onFocus: (ev: React.FocusEvent) => void; onFocus: (ev: React.FocusEvent) => void;
onBlur: (ev: React.FocusEvent) => void; onBlur: (ev: React.FocusEvent) => void;
onResize: () => void;
onListCollapse?: (isExpanded: boolean) => void; onListCollapse?: (isExpanded: boolean) => void;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
isMinimized: boolean; isMinimized: boolean;
@ -404,7 +405,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const newSublists = objectWithOnly(newLists, newListIds); const newSublists = objectWithOnly(newLists, newListIds);
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v)); const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
this.setState({sublists, isNameFiltering}); this.setState({sublists, isNameFiltering}, () => {
this.props.onResize();
});
} }
}; };

View file

@ -454,8 +454,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const sublist = possibleSticky.parentElement.parentElement; const sublist = possibleSticky.parentElement.parentElement;
const list = sublist.parentElement.parentElement; const list = sublist.parentElement.parentElement;
// the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky // the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
const isAtTop = list.scrollTop <= HEADER_HEIGHT; const listScrollTop = Math.round(list.scrollTop);
const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight; const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT);
const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight);
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop'); const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom'); const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');

View file

@ -24,7 +24,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.RoomUpgradeWarningBar") @replaceableComponent("views.rooms.RoomUpgradeWarningBar")
export default class RoomUpgradeWarningBar extends React.Component { export default class RoomUpgradeWarningBar extends React.PureComponent {
static propTypes = { static propTypes = {
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
recommendation: PropTypes.object.isRequired, recommendation: PropTypes.object.isRequired,

View file

@ -16,23 +16,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import AccessibleButton from '../elements/AccessibleButton';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
const {onClick} = props;
return (
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>
);
}
/* /*
* A stripped-down room header used for things like the user settings * A stripped-down room header used for things like the user settings
* and room directory. * and room directory.
@ -41,18 +27,13 @@ export function CancelButton(props) {
export default class SimpleRoomHeader extends React.Component { export default class SimpleRoomHeader extends React.Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
onCancelClick: PropTypes.func,
// `src` to a TintableSvg. Optional. // `src` to a TintableSvg. Optional.
icon: PropTypes.string, icon: PropTypes.string,
}; };
render() { render() {
let cancelButton;
let icon; let icon;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.icon) { if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg'); const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg icon = <TintableSvg
@ -66,7 +47,6 @@ export default class SimpleRoomHeader extends React.Component {
<div className="mx_RoomHeader_simpleHeader"> <div className="mx_RoomHeader_simpleHeader">
{ icon } { icon }
{ this.props.title } { this.props.title }
{ cancelButton }
</div> </div>
</div> </div>
); );

View file

@ -22,7 +22,7 @@ import FocusLock from "react-focus-lock";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu"; import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
import createRoom, {IStateEvent, Preset} from "../../../createRoom"; import createRoom from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {SpaceAvatar} from "./SpaceBasicSettings"; import {SpaceAvatar} from "./SpaceBasicSettings";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
@ -33,6 +33,8 @@ import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field"; import Field from "../elements/Field";
import withValidation from "../elements/Validation"; import withValidation from "../elements/Validation";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import { Preset } from "matrix-js-sdk/src/@types/partials";
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => { const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return ( return (
@ -81,7 +83,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
return; return;
} }
const initialState: IStateEvent[] = [ const initialState: ICreateRoomStateEvent[] = [
{ {
type: EventType.RoomHistoryVisibility, type: EventType.RoomHistoryVisibility,
content: { content: {

View file

@ -31,7 +31,6 @@ const RoomContext = createContext<IState>({
canPeek: false, canPeek: false,
showApps: false, showApps: false,
isPeeking: false, isPeeking: false,
showReadReceipts: true,
showRightPanel: true, showRightPanel: true,
joining: false, joining: false,
atEndOfLiveTimeline: true, atEndOfLiveTimeline: true,
@ -41,6 +40,12 @@ const RoomContext = createContext<IState>({
canReact: false, canReact: false,
canReply: false, canReply: false,
layout: Layout.Group, layout: Layout.Group,
lowBandwidth: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false, matrixClientIsReady: false,
dragCounter: 0, dragCounter: 0,
}); });

View file

@ -35,53 +35,15 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
import SpaceStore from "./stores/SpaceStore"; import SpaceStore from "./stores/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space"; import { makeSpaceParentEvent } from "./utils/space";
import { Action } from "./dispatcher/actions" import { Action } from "./dispatcher/actions"
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
// we define a number of interfaces which take their names from the js-sdk // we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */ /* eslint-disable camelcase */
// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
export enum Visibility {
Public = "public",
Private = "private",
}
export enum Preset {
PrivateChat = "private_chat",
TrustedPrivateChat = "trusted_private_chat",
PublicChat = "public_chat",
}
interface Invite3PID {
id_server: string;
id_access_token?: string; // this gets injected by the js-sdk
medium: string;
address: string;
}
export interface IStateEvent {
type: string;
state_key?: string; // defaults to an empty string
content: object;
}
interface ICreateOpts {
visibility?: Visibility;
room_alias_name?: string;
name?: string;
topic?: string;
invite?: string[];
invite_3pid?: Invite3PID[];
room_version?: string;
creation_content?: object;
initial_state?: IStateEvent[];
preset?: Preset;
is_direct?: boolean;
power_level_content_override?: object;
}
export interface IOpts { export interface IOpts {
dmUserId?: string; dmUserId?: string;
createOpts?: ICreateOpts; createOpts?: ICreateRoomOpts;
spinner?: boolean; spinner?: boolean;
guestAccess?: boolean; guestAccess?: boolean;
encryption?: boolean; encryption?: boolean;
@ -91,12 +53,6 @@ export interface IOpts {
parentSpace?: Room; parentSpace?: Room;
} }
export interface IInvite3PID {
id_server: string,
medium: 'email',
address: string,
}
/** /**
* Create a new room, and switch to it. * Create a new room, and switch to it.
* *
@ -136,7 +92,7 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat; const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
// set some defaults for the creation // set some defaults for the creation
const createOpts = opts.createOpts || {}; const createOpts: ICreateRoomOpts = opts.createOpts || {};
createOpts.preset = createOpts.preset || defaultPreset; createOpts.preset = createOpts.preset || defaultPreset;
createOpts.visibility = createOpts.visibility || Visibility.Private; createOpts.visibility = createOpts.visibility || Visibility.Private;
if (opts.dmUserId && createOpts.invite === undefined) { if (opts.dmUserId && createOpts.invite === undefined) {

View file

@ -35,8 +35,8 @@ export const useSettingValue = <T>(settingName: string, roomId: string = null, e
}; };
// Hook to fetch whether a feature is enabled and dynamically update when that changes // Hook to fetch whether a feature is enabled and dynamically update when that changes
export const useFeatureEnabled = (featureName: string, roomId: string = null) => { export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => {
const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId)); const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
useEffect(() => { useEffect(() => {
const ref = SettingsStore.watchSetting(featureName, roomId, () => { const ref = SettingsStore.watchSetting(featureName, roomId, () => {

View file

@ -78,13 +78,13 @@
"Invite new community members": "Convida nous membres a la comunitat", "Invite new community members": "Convida nous membres a la comunitat",
"Invite to Community": "Convida a la comunitat", "Invite to Community": "Convida a la comunitat",
"Which rooms would you like to add to this community?": "Quines sales vols afegir a aquesta comunitat?", "Which rooms would you like to add to this community?": "Quines sales vols afegir a aquesta comunitat?",
"Show these rooms to non-members on the community page and room list?": "Vols mostrar aquestes sales a la pàgina de la comunitat i a la llista de sales per als que no hi son membres?", "Show these rooms to non-members on the community page and room list?": "Voleu mostrar aquestes sales a la pàgina de la comunitat i al llistat de sales, als qui no en siguin membres?",
"Add rooms to the community": "Afegeix sales a la comunitat", "Add rooms to the community": "Afegeix sales a la comunitat",
"Add to community": "Afegeix a la comunitat", "Add to community": "Afegeix a la comunitat",
"Failed to invite the following users to %(groupId)s:": "No s'han pogut convidar a %(groupId)s els següents usuaris:", "Failed to invite the following users to %(groupId)s:": "No s'han pogut convidar a %(groupId)s els següents usuaris:",
"Failed to invite users to community": "No s'han pogut convidar els usuaris a la comunitat", "Failed to invite users to community": "No s'ha pogut convidar els usuaris a la comunitat",
"Failed to invite users to %(groupId)s": "No s'han pogut convidar els usuaris a %(groupId)s", "Failed to invite users to %(groupId)s": "No s'han pogut convidar els usuaris a %(groupId)s",
"Failed to add the following rooms to %(groupId)s:": "No s'han pogut afegir a %(groupId)s les següents sales:", "Failed to add the following rooms to %(groupId)s:": "No s'ha pogut afegir a %(groupId)s les següents sales:",
"%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s no té permís per enviar-te notificacions, comprova la configuració del teu navegador", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s no té permís per enviar-te notificacions, comprova la configuració del teu navegador",
"%(brand)s was not given permission to send notifications - please try again": "%(brand)s no ha rebut cap permís per enviar notificacions, torna-ho a provar", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s no ha rebut cap permís per enviar notificacions, torna-ho a provar",
"Unable to enable Notifications": "No s'han pogut activar les notificacions", "Unable to enable Notifications": "No s'han pogut activar les notificacions",
@ -371,7 +371,7 @@
"Communities": "Comunitats", "Communities": "Comunitats",
"Home": "Inici", "Home": "Inici",
"Manage Integrations": "Gestiona les integracions", "Manage Integrations": "Gestiona les integracions",
"%(nameList)s %(transitionList)s": "%(transitionList)s%(nameList)s", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s s'hi han unit", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s s'hi han unit",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)ss'ha unit", "%(oneUser)sjoined %(count)s times|one": "%(oneUser)ss'ha unit",
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s han sortit", "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s han sortit",
@ -722,7 +722,7 @@
"Failed to invite users to the room:": "No s'han pogut convidar els usuaris a la sala:", "Failed to invite users to the room:": "No s'han pogut convidar els usuaris a la sala:",
"Missing roomId.": "Falta l'ID de sala.", "Missing roomId.": "Falta l'ID de sala.",
"Searches DuckDuckGo for results": "Cerca al DuckDuckGo els resultats", "Searches DuckDuckGo for results": "Cerca al DuckDuckGo els resultats",
"Changes your display nickname": "Canvia el teu àlies de visualització", "Changes your display nickname": "Canvia l'àlies a mostrar",
"Invites user with given id to current room": "Convida a la sala actual l'usuari amb l'ID indicat", "Invites user with given id to current room": "Convida a la sala actual l'usuari amb l'ID indicat",
"Kicks user with given id": "Expulsa l'usuari amb l'ID indicat", "Kicks user with given id": "Expulsa l'usuari amb l'ID indicat",
"Bans user with given id": "Bandeja l'usuari amb l'ID indicat", "Bans user with given id": "Bandeja l'usuari amb l'ID indicat",
@ -854,7 +854,7 @@
"Changes your avatar in all rooms": "Canvia el teu avatar en totes les sales", "Changes your avatar in all rooms": "Canvia el teu avatar en totes les sales",
"Changes your avatar in this current room only": "Canvia el teu avatar només en aquesta sala actual", "Changes your avatar in this current room only": "Canvia el teu avatar només en aquesta sala actual",
"Changes the avatar of the current room": "Canvia l'avatar de la sala actual", "Changes the avatar of the current room": "Canvia l'avatar de la sala actual",
"Changes your display nickname in the current room only": "Canvia el teu àlies de visualització només en la sala actual", "Changes your display nickname in the current room only": "Canvia el teu àlies a mostrar només en la sala actual",
"Double check that your server supports the room version chosen and try again.": "Comprova que el teu servidor és compatible amb la versió de sala que has triat i torna-ho a intentar.", "Double check that your server supports the room version chosen and try again.": "Comprova que el teu servidor és compatible amb la versió de sala que has triat i torna-ho a intentar.",
"You do not have the required permissions to use this command.": "No disposes dels permisos necessaris per utilitzar aquesta ordre.", "You do not have the required permissions to use this command.": "No disposes dels permisos necessaris per utilitzar aquesta ordre.",
"Sends a message as html, without interpreting it as markdown": "Envia un missatge com a html sense interpretar-lo com a markdown", "Sends a message as html, without interpreting it as markdown": "Envia un missatge com a html sense interpretar-lo com a markdown",
@ -951,5 +951,7 @@
"Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic.", "Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic.",
"Unable to access webcam / microphone": "No s'ha pogut accedir a la càmera web / micròfon", "Unable to access webcam / microphone": "No s'ha pogut accedir a la càmera web / micròfon",
"Unable to access microphone": "No s'ha pogut accedir al micròfon", "Unable to access microphone": "No s'ha pogut accedir al micròfon",
"Explore rooms": "Explora sales" "Explore rooms": "Explora sales",
"%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sno ha fet canvis",
"%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops"
} }

View file

@ -3292,5 +3292,11 @@
"See when people join, leave, or are invited to your active room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do vaší aktivní místnosti", "See when people join, leave, or are invited to your active room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do vaší aktivní místnosti",
"Kick, ban, or invite people to this room, and make you leave": "Vykopnout, vykázat, pozvat lidi do této místnosti nebo odejít", "Kick, ban, or invite people to this room, and make you leave": "Vykopnout, vykázat, pozvat lidi do této místnosti nebo odejít",
"Kick, ban, or invite people to your active room, and make you leave": "Vykopnout, vykázat, pozvat lidi do vaší aktivní místnosti nebo odejít", "Kick, ban, or invite people to your active room, and make you leave": "Vykopnout, vykázat, pozvat lidi do vaší aktivní místnosti nebo odejít",
"See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti" "See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti",
"Currently joining %(count)s rooms|one": "Momentálně se připojuje %(count)s místnost",
"Currently joining %(count)s rooms|other": "Momentálně se připojuje %(count)s místností",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Vyzkoušejte jiná slova nebo zkontrolujte překlepy. Některé výsledky nemusí být viditelné, protože jsou soukromé a potřebujete k nim pozvánku.",
"No results for \"%(query)s\"": "Žádné výsledky pro \"%(query)s\"",
"The user you called is busy.": "Volaný uživatel je zaneprázdněn.",
"User Busy": "Uživatel zaneprázdněn"
} }

View file

@ -3343,5 +3343,14 @@
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Dein Feedback hilfst uns, die Spaces zu verbessern. Je genauer, desto besser.", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Dein Feedback hilfst uns, die Spaces zu verbessern. Je genauer, desto besser.",
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Durchs Verlassen lädt %(brand)s mit deaktivierten Spaces neu. Danach kannst du Communities und Custom Tags wieder verwenden.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Durchs Verlassen lädt %(brand)s mit deaktivierten Spaces neu. Danach kannst du Communities und Custom Tags wieder verwenden.",
"sends space invaders": "sendet Space Invaders", "sends space invaders": "sendet Space Invaders",
"Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen" "Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen",
"Space Autocomplete": "Spaces automatisch vervollständigen",
"Currently joining %(count)s rooms|one": "Betrete %(count)s Raum",
"Currently joining %(count)s rooms|other": "Betrete %(count)s Räume",
"Go to my space": "Zu meinem Space",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Überprüfe auf Tippfehler oder verwende andere Suchbegriffe. Beachte, dass Ergebnisse aus privaten Räumen, in die du nicht eingeladen wurdest, nicht angezeigt werden.",
"See when people join, leave, or are invited to this room": "Anzeigen, wenn Leute eingeladen werden oder den Raum betreten und verlassen",
"The user you called is busy.": "Der angerufene Benutzer ist momentan beschäftigt.",
"User Busy": "Benutzer beschäftigt",
"No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\""
} }

View file

@ -556,8 +556,8 @@
"%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.", "%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.", "%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.",
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).", "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
@ -1473,7 +1473,6 @@
"Encrypting your message...": "Encrypting your message...", "Encrypting your message...": "Encrypting your message...",
"Your message was sent": "Your message was sent", "Your message was sent": "Your message was sent",
"Failed to send": "Failed to send", "Failed to send": "Failed to send",
"Please select the destination room for this message": "Please select the destination room for this message",
"Scroll to most recent messages": "Scroll to most recent messages", "Scroll to most recent messages": "Scroll to most recent messages",
"Close preview": "Close preview", "Close preview": "Close preview",
"and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|other": "and %(count)s others...",
@ -1510,6 +1509,8 @@
"Invite to just this room": "Invite to just this room", "Invite to just this room": "Invite to just this room",
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
"This is the start of <roomName/>.": "This is the start of <roomName/>.", "This is the start of <roomName/>.": "This is the start of <roomName/>.",
"Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. <a>Enable encryption in settings.</a>": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. <a>Enable encryption in settings.</a>",
"End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled",
"Unpin": "Unpin", "Unpin": "Unpin",
"View message": "View message", "View message": "View message",
"%(duration)ss": "%(duration)ss", "%(duration)ss": "%(duration)ss",
@ -1717,8 +1718,8 @@
"The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to", "The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to",
"Yours, or the other users internet connection": "Yours, or the other users internet connection", "Yours, or the other users internet connection": "Yours, or the other users internet connection",
"Yours, or the other users session": "Yours, or the other users session", "Yours, or the other users session": "Yours, or the other users session",
"Youre all caught up": "Youre all caught up", "Nothing pinned, yet": "Nothing pinned, yet",
"You have no visible notifications.": "You have no visible notifications.", "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
"Pinned messages": "Pinned messages", "Pinned messages": "Pinned messages",
"Room Info": "Room Info", "Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
@ -1926,6 +1927,8 @@
"Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widgets do not use message encryption.": "Widgets do not use message encryption.",
"Widget added by": "Widget added by", "Widget added by": "Widget added by",
"This widget may use cookies.": "This widget may use cookies.", "This widget may use cookies.": "This widget may use cookies.",
"Error loading Widget": "Error loading Widget",
"Error - Mixed content": "Error - Mixed content",
"Popout widget": "Popout widget", "Popout widget": "Popout widget",
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files", "Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages", "Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
@ -2204,6 +2207,13 @@
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.", "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Report a bug": "Report a bug", "Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.", "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"You don't have permission to do this": "You don't have permission to do this",
"Sending": "Sending",
"Sent": "Sent",
"Open link": "Open link",
"Forward message": "Forward message",
"Message preview": "Message preview",
"Search for rooms or people": "Search for rooms or people",
"Confirm abort of host creation": "Confirm abort of host creation", "Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
"Abort": "Abort", "Abort": "Abort",
@ -2247,6 +2257,9 @@
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).", "Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
"Go": "Go", "Go": "Go",
"Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.",
"If you can't see who youre looking for, send them your invite link below.": "If you can't see who youre looking for, send them your invite link below.",
"Or send invite link": "Or send invite link",
"Unnamed Space": "Unnamed Space", "Unnamed Space": "Unnamed Space",
"Invite to %(roomName)s": "Invite to %(roomName)s", "Invite to %(roomName)s": "Invite to %(roomName)s",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.", "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
@ -2628,6 +2641,8 @@
"Create a new community": "Create a new community", "Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
"Communities are changing to Spaces": "Communities are changing to Spaces", "Communities are changing to Spaces": "Communities are changing to Spaces",
"Youre all caught up": "Youre all caught up",
"You have no visible notifications.": "You have no visible notifications.",
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
"%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.",
"The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.",
@ -2662,7 +2677,6 @@
"Some of your messages have not been sent": "Some of your messages have not been sent", "Some of your messages have not been sent": "Some of your messages have not been sent",
"Delete all": "Delete all", "Delete all": "Delete all",
"Retry all": "Retry all", "Retry all": "Retry all",
"Sending": "Sending",
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete", "You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",

View file

@ -1591,10 +1591,10 @@
"Room Autocomplete": "Memkompletigo de ĉambroj", "Room Autocomplete": "Memkompletigo de ĉambroj",
"User Autocomplete": "Memkompletigo de uzantoj", "User Autocomplete": "Memkompletigo de uzantoj",
"Custom (%(level)s)": "Propra (%(level)s)", "Custom (%(level)s)": "Propra (%(level)s)",
"%(senderName)s placed a voice call.": "%(senderName)s metis voĉvokon.", "%(senderName)s placed a voice call.": "%(senderName)s ekigis voĉvokon.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s metis voĉvokon. (mankas subteno en ĉi tiu foliumilo)", "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s ekigis voĉvokon. (mankas subteno en ĉi tiu foliumilo)",
"%(senderName)s placed a video call.": "%(senderName)s metis vidvokon.", "%(senderName)s placed a video call.": "%(senderName)s ekigis vidvokon.",
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s metis vidvokon. (mankas subteno en ĉi tiu foliumilo)", "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s ekigis vidvokon. (mankas subteno en ĉi tiu foliumilo)",
"Try out new ways to ignore people (experimental)": "Elprovi novajn manierojn malatenti personojn (eksperimente)", "Try out new ways to ignore people (experimental)": "Elprovi novajn manierojn malatenti personojn (eksperimente)",
"Match system theme": "Similiĝi la sisteman haŭton", "Match system theme": "Similiĝi la sisteman haŭton",
"My Ban List": "Mia listo de forbaroj", "My Ban List": "Mia listo de forbaroj",
@ -3320,5 +3320,11 @@
"Reset event store": "Restarigi deponejon de okazoj", "Reset event store": "Restarigi deponejon de okazoj",
"If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata", "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata",
"You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj", "You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj",
"Reset event store?": "Ĉu restarigi deponejon de okazoj?" "Reset event store?": "Ĉu restarigi deponejon de okazoj?",
"Currently joining %(count)s rooms|one": "Nun aliĝante al %(count)s ĉambro",
"Currently joining %(count)s rooms|other": "Nun aliĝante al %(count)s ĉambroj",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provu aliajn vortojn aŭ kontorolu, ĉu vi ne tajperaris. Iuj rezultoj eble ne videblos, ĉar ili estas privataj kaj vi bezonus inviton por aliĝi.",
"No results for \"%(query)s\"": "Neniuj rezultoj por «%(query)s»",
"The user you called is busy.": "La uzanto, kiun vi vokis, estas okupata.",
"User Busy": "Uzanto estas okupata"
} }

View file

@ -3315,5 +3315,11 @@
"See when people join, leave, or are invited to your active room": "Ver cuando alguien se una, salga o se le invite a tu sala activa", "See when people join, leave, or are invited to your active room": "Ver cuando alguien se una, salga o se le invite a tu sala activa",
"Kick, ban, or invite people to this room, and make you leave": "Expulsar, vetar o invitar personas a esta sala, y hacerte salir de ella", "Kick, ban, or invite people to this room, and make you leave": "Expulsar, vetar o invitar personas a esta sala, y hacerte salir de ella",
"Kick, ban, or invite people to your active room, and make you leave": "Expulsar, vetar o invitar a gente a tu sala activa, o hacerte salir", "Kick, ban, or invite people to your active room, and make you leave": "Expulsar, vetar o invitar a gente a tu sala activa, o hacerte salir",
"See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala" "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prueba con sinónimos o revisa si te has equivocado al escribir. Puede que algunos resultados no sean visibles si son privados y necesites que te inviten para verlos.",
"Currently joining %(count)s rooms|one": "Entrando en %(count)s sala",
"Currently joining %(count)s rooms|other": "Entrando en %(count)s salas",
"No results for \"%(query)s\"": "Ningún resultado para «%(query)s»",
"The user you called is busy.": "La persona a la que has llamado está ocupada.",
"User Busy": "Persona ocupada"
} }

View file

@ -3345,5 +3345,14 @@
"Send and receive voice messages": "Saada ja võta vastu häälsõnumeid", "Send and receive voice messages": "Saada ja võta vastu häälsõnumeid",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Sinu tagasiside aitab teha kogukonnakeskuseid paremaks. Mida detailsemalt sa oma arvamust kirjeldad, seda parem.", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Sinu tagasiside aitab teha kogukonnakeskuseid paremaks. Mida detailsemalt sa oma arvamust kirjeldad, seda parem.",
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.",
"Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud" "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud",
"sends space invaders": "korraldab ühe pisikese tulnukate vallutusretke",
"Sends the given message with a space themed effect": "Saadab antud sõnumi kosmoseteemalise efektiga",
"Go to my space": "Palun vaata minu kogukonnakeskust",
"User Busy": "Kasutaja on hõivatud",
"The user you called is busy.": "Kasutaja, kellele sa helistasid, on hõivatud.",
"No results for \"%(query)s\"": "Päringule „%(query)s“ pole vastuseid",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Proovi muid otsingusõnu või kontrolli, et neis polnud trükivigu. Kuna mõned otsingutulemused on privaatsed ja sa vajad kutset nende nägemiseks, siis kõiki tulemusi siin ei pruugi näha olla.",
"Currently joining %(count)s rooms|other": "Parasjagu liitun %(count)s jututoaga",
"Currently joining %(count)s rooms|one": "Parasjagu liitun %(count)s jututoaga"
} }

View file

@ -1873,7 +1873,7 @@
"This user has not verified all of their sessions.": "Cet utilisateur na pas vérifié toutes ses sessions.", "This user has not verified all of their sessions.": "Cet utilisateur na pas vérifié toutes ses sessions.",
"You have verified this user. This user has verified all of their sessions.": "Vous avez vérifié cet utilisateur. Cet utilisateur a vérifié toutes ses sessions.", "You have verified this user. This user has verified all of their sessions.": "Vous avez vérifié cet utilisateur. Cet utilisateur a vérifié toutes ses sessions.",
"Someone is using an unknown session": "Quelquun utilise une session inconnue", "Someone is using an unknown session": "Quelquun utilise une session inconnue",
"Mod": "Modo", "Mod": "Modérateur",
"Your key share request has been sent - please check your other sessions for key share requests.": "Votre demande de partage de clé a été envoyée vérifiez les demandes de partage de clé sur vos autres sessions.", "Your key share request has been sent - please check your other sessions for key share requests.": "Votre demande de partage de clé a été envoyée vérifiez les demandes de partage de clé sur vos autres sessions.",
"Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Les demandes de partage de clé sont envoyées à vos autres sessions automatiquement. Si vous avez rejeté ou ignoré la demande de partage de clé sur vos autres sessions, cliquez ici pour redemander les clés pour cette session.", "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Les demandes de partage de clé sont envoyées à vos autres sessions automatiquement. Si vous avez rejeté ou ignoré la demande de partage de clé sur vos autres sessions, cliquez ici pour redemander les clés pour cette session.",
"If your other sessions do not have the key for this message you will not be able to decrypt them.": "Si vos autres sessions nont pas la clé pour ce message vous ne pourrez pas le déchiffrer.", "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Si vos autres sessions nont pas la clé pour ce message vous ne pourrez pas le déchiffrer.",
@ -2842,7 +2842,7 @@
"Change the topic of your active room": "Changer le sujet dans le salon actuel", "Change the topic of your active room": "Changer le sujet dans le salon actuel",
"Change the topic of this room": "Changer le sujet de ce salon", "Change the topic of this room": "Changer le sujet de ce salon",
"Send stickers into this room": "Envoyer des autocollants dans ce salon", "Send stickers into this room": "Envoyer des autocollants dans ce salon",
"Remain on your screen when viewing another room, when running": "Reste sur votre écran quand vous regardez un autre salon lors de lappel", "Remain on your screen when viewing another room, when running": "Reste sur votre écran lors de lappel quand vous regardez un autre salon",
"Takes the call in the current room off hold": "Reprend lappel en attente dans ce salon", "Takes the call in the current room off hold": "Reprend lappel en attente dans ce salon",
"Places the call in the current room on hold": "Met lappel dans ce salon en attente", "Places the call in the current room on hold": "Met lappel dans ce salon en attente",
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Ajoute (╯°□°)╯︵ ┻━┻ en préfixe du message", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Ajoute (╯°□°)╯︵ ┻━┻ en préfixe du message",
@ -2861,14 +2861,14 @@
"Send videos as you in this room": "Envoie des vidéos sous votre nom dans ce salon", "Send videos as you in this room": "Envoie des vidéos sous votre nom dans ce salon",
"See images posted to this room": "Voir les images publiées dans ce salon", "See images posted to this room": "Voir les images publiées dans ce salon",
"See images posted to your active room": "Voir les images publiées dans votre salon actif", "See images posted to your active room": "Voir les images publiées dans votre salon actif",
"See messages posted to your active room": "Voir les messages publiés dans votre salon actif", "See messages posted to your active room": "Voir les messages envoyés dans le salon actuel",
"See messages posted to this room": "Voir les messages publiés dans ce salon", "See messages posted to this room": "Voir les messages publiés dans ce salon",
"Send messages as you in your active room": "Envoie des messages sous votre nom dans votre salon actif", "Send messages as you in your active room": "Envoie des messages sous votre nom dans votre salon actif",
"Send messages as you in this room": "Envoie des messages sous votre nom dans ce salon", "Send messages as you in this room": "Envoie des messages sous votre nom dans ce salon",
"The <b>%(capability)s</b> capability": "La capacité <b>%(capability)s</b>", "The <b>%(capability)s</b> capability": "La capacité <b>%(capability)s</b>",
"See <b>%(eventType)s</b> events posted to your active room": "Voir les événements <b>%(eventType)s</b> publiés dans votre salon actuel", "See <b>%(eventType)s</b> events posted to your active room": "Voir les événements <b>%(eventType)s</b> publiés dans votre salon actuel",
"Send <b>%(eventType)s</b> events as you in your active room": "Envoie des événements <b>%(eventType)s</b> sous votre nom dans votre salon actuel", "Send <b>%(eventType)s</b> events as you in your active room": "Envoie des événements <b>%(eventType)s</b> sous votre nom dans votre salon actuel",
"See <b>%(eventType)s</b> events posted to this room": "Voir les événements <b>%(eventType)s</b> publiés dans ce salon", "See <b>%(eventType)s</b> events posted to this room": "Voir les événements <b>%(eventType)s</b> envoyés dans ce salon",
"Send <b>%(eventType)s</b> events as you in this room": "Envoie des événements <b>%(eventType)s</b> sous votre nom dans ce salon", "Send <b>%(eventType)s</b> events as you in this room": "Envoie des événements <b>%(eventType)s</b> sous votre nom dans ce salon",
"Send stickers to your active room as you": "Envoie des autocollants sous votre nom dans le salon actuel", "Send stickers to your active room as you": "Envoie des autocollants sous votre nom dans le salon actuel",
"Continue with %(ssoButtons)s": "Continuer avec %(ssoButtons)s", "Continue with %(ssoButtons)s": "Continuer avec %(ssoButtons)s",
@ -2930,7 +2930,7 @@
"Don't miss a reply": "Ne ratez pas une réponse", "Don't miss a reply": "Ne ratez pas une réponse",
"See <b>%(msgtype)s</b> messages posted to your active room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans le salon actuel", "See <b>%(msgtype)s</b> messages posted to your active room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans le salon actuel",
"See <b>%(msgtype)s</b> messages posted to this room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans ce salon", "See <b>%(msgtype)s</b> messages posted to this room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans ce salon",
"Send <b>%(msgtype)s</b> messages as you in this room": "Envoie des messages de type<b>%(msgtype)s</b> sous votre nom dans ce salon", "Send <b>%(msgtype)s</b> messages as you in this room": "Envoie les messages de type <b>%(msgtype)s</b> sous votre nom dans ce salon",
"Send <b>%(msgtype)s</b> messages as you in your active room": "Envoie des messages de type <b>%(msgtype)s</b> sous votre nom dans votre salon actif", "Send <b>%(msgtype)s</b> messages as you in your active room": "Envoie des messages de type <b>%(msgtype)s</b> sous votre nom dans votre salon actif",
"See general files posted to your active room": "Voir les fichiers postés dans votre salon actuel", "See general files posted to your active room": "Voir les fichiers postés dans votre salon actuel",
"See general files posted to this room": "Voir les fichiers postés dans ce salon", "See general files posted to this room": "Voir les fichiers postés dans ce salon",
@ -3034,7 +3034,7 @@
"Send text messages as you in this room": "Envoyez des messages textuels sous votre nom dans ce salon", "Send text messages as you in this room": "Envoyez des messages textuels sous votre nom dans ce salon",
"See when the name changes in your active room": "Suivre les changements de nom dans le salon actif", "See when the name changes in your active room": "Suivre les changements de nom dans le salon actif",
"Change which room, message, or user you're viewing": "Changer le salon, message, ou la personne que vous visualisez", "Change which room, message, or user you're viewing": "Changer le salon, message, ou la personne que vous visualisez",
"Change which room you're viewing": "Changer le salon que vous visualisez", "Change which room you're viewing": "Changer le salon que vous êtes en train de lire",
"Remain on your screen while running": "Reste sur votre écran pendant lexécution", "Remain on your screen while running": "Reste sur votre écran pendant lexécution",
"%(senderName)s has updated the widget layout": "%(senderName)s a mis à jour la disposition du widget", "%(senderName)s has updated the widget layout": "%(senderName)s a mis à jour la disposition du widget",
"Converts the DM to a room": "Transforme la conversation privée en salon", "Converts the DM to a room": "Transforme la conversation privée en salon",
@ -3111,7 +3111,7 @@
"No permissions": "Aucune permission", "No permissions": "Aucune permission",
"Remove from Space": "Supprimer de lespace", "Remove from Space": "Supprimer de lespace",
"Undo": "Annuler", "Undo": "Annuler",
"Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.": "Votre message na pas été envoyé car ce serveur daccueil a été banni par son administrateur. Merci de <a>contacter votre administrateur de service</a> pour poursuivre lusage de ce service.", "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.": "Votre message na pas été envoyé car ce serveur daccueil a été bloqué par son administrateur. Merci de <a>contacter votre administrateur de service</a> pour continuer à utiliser le service.",
"Are you sure you want to leave the space '%(spaceName)s'?": "Êtes-vous sûr de vouloir quitter lespace « %(spaceName)s » ?", "Are you sure you want to leave the space '%(spaceName)s'?": "Êtes-vous sûr de vouloir quitter lespace « %(spaceName)s » ?",
"This space is not public. You will not be able to rejoin without an invite.": "Cet espace nest pas public. Vous ne pourrez pas le rejoindre sans invitation.", "This space is not public. You will not be able to rejoin without an invite.": "Cet espace nest pas public. Vous ne pourrez pas le rejoindre sans invitation.",
"Start audio stream": "Démarrer une diffusion audio", "Start audio stream": "Démarrer une diffusion audio",
@ -3352,5 +3352,11 @@
"sends space invaders": "Envoie les Space Invaders", "sends space invaders": "Envoie les Space Invaders",
"Sends the given message with a space themed effect": "Envoyer le message avec un effet lié au thème de lespace", "Sends the given message with a space themed effect": "Envoyer le message avec un effet lié au thème de lespace",
"See when people join, leave, or are invited to your active room": "Afficher quand des personnes rejoignent, partent, ou sont invités dans votre salon actif", "See when people join, leave, or are invited to your active room": "Afficher quand des personnes rejoignent, partent, ou sont invités dans votre salon actif",
"Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir" "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir",
"Currently joining %(count)s rooms|one": "Vous êtes en train de rejoindre %(count)s salon",
"Currently joining %(count)s rooms|other": "Vous êtes en train de rejoindre %(count)s salons",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Essayez d'autres mots ou vérifiez les fautes de frappe. Certains salons peuvent ne pas être visibles car ils sont privés et vous devez être invité pour les rejoindre.",
"No results for \"%(query)s\"": "Aucun résultat pour « %(query)s »",
"The user you called is busy.": "Lutilisateur que vous avez appelé est indisponible.",
"User Busy": "Utilisateur indisponible"
} }

View file

@ -3375,5 +3375,11 @@
"See when people join, leave, or are invited to your active room": "Mira cando alguén se une, sae ou é convidada á túa sala activa", "See when people join, leave, or are invited to your active room": "Mira cando alguén se une, sae ou é convidada á túa sala activa",
"Kick, ban, or invite people to your active room, and make you leave": "Expulsa, veta ou convida a persoas á túa sala activa, e fai que saias", "Kick, ban, or invite people to your active room, and make you leave": "Expulsa, veta ou convida a persoas á túa sala activa, e fai que saias",
"See when people join, leave, or are invited to this room": "Mira cando se une alguén, sae ou é convidada a esta sala", "See when people join, leave, or are invited to this room": "Mira cando se une alguén, sae ou é convidada a esta sala",
"Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias" "Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias",
"Currently joining %(count)s rooms|one": "Neste intre estás en %(count)s sala",
"Currently joining %(count)s rooms|other": "Neste intre estás en %(count)s salas",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Intentao con outras palabras e fíxate nos erros de escritura. Algúns resultados poderían non ser visibles porque son privados e precisas un convite.",
"No results for \"%(query)s\"": "Sen resultados para \"%(query)s\"",
"The user you called is busy.": "A persoa á que chamas está ocupada.",
"User Busy": "Usuaria ocupada"
} }

View file

@ -5,5 +5,205 @@
"The platform you're on": "Platforma na kojoj se nalazite", "The platform you're on": "Platforma na kojoj se nalazite",
"The version of %(brand)s": "Verzija %(brand)s", "The version of %(brand)s": "Verzija %(brand)s",
"Your language of choice": "Izabrani jezik", "Your language of choice": "Izabrani jezik",
"Dismiss": "Odbaci" "Dismiss": "Odbaci",
"France": "Francuska",
"Finland": "Finska",
"Fiji": "Fiji",
"Faroe Islands": "Farski otoci",
"Falkland Islands": "Falklandski otoci",
"Ethiopia": "Etiopija",
"Estonia": "Estonija",
"Eritrea": "Eritreja",
"Equatorial Guinea": "Ekvatorska Gvineja",
"El Salvador": "El Salvador",
"Egypt": "Egipat",
"Ecuador": "Ekvador",
"Dominican Republic": "Dominikanska Republika",
"Dominica": "Dominika",
"Djibouti": "Džibuti",
"Denmark": "Danska",
"Côte dIvoire": "Obala Bjelokosti",
"Czech Republic": "Češka",
"Cyprus": "Cipar",
"Curaçao": "Curaçao",
"Cuba": "Kuba",
"Croatia": "Hrvatska",
"Costa Rica": "Kostarika",
"Cook Islands": "Cookovo Otočje",
"Congo - Kinshasa": "Kongo - Kinshasa",
"Congo - Brazzaville": "Republika Kongo",
"Comoros": "Komori",
"Colombia": "Kolumbija",
"Cocos (Keeling) Islands": "Kokosovi (Keeling) otoci",
"Christmas Island": "Uskršnji otoci",
"China": "Kina",
"Chile": "Čile",
"Chad": "Čad",
"Central African Republic": "Srednjoafrička Republika",
"Cayman Islands": "Kajmanski otoci",
"Caribbean Netherlands": "Karipska Nizozemska",
"Cape Verde": "Zelenortski Otoci",
"Canada": "Kanada",
"Cameroon": "Kamerun",
"Cambodia": "Kambodža",
"Burundi": "Burundi",
"Burkina Faso": "Burkina Faso",
"Bulgaria": "Bugarska",
"Brunei": "Brunej",
"British Virgin Islands": "Britanski djevičanski otoci",
"British Indian Ocean Territory": "Britanski teritorij Indijskog oceana",
"Brazil": "Brazil",
"Bouvet Island": "Otok Bouvet",
"Botswana": "Bocvana",
"Bosnia": "Bosna i Hercegovina",
"Bolivia": "Bolivija",
"Bhutan": "Butan",
"Bermuda": "Bermuda",
"Benin": "Benin",
"Belize": "Belize",
"Belgium": "Belgija",
"Belarus": "Bjelorusija",
"Barbados": "Barbados",
"Bangladesh": "Bangladeš",
"Bahrain": "Bahrein",
"Bahamas": "Bahami",
"Azerbaijan": "Azerbejdžan",
"Austria": "Austrija",
"Australia": "Australija",
"Aruba": "Aruba",
"Armenia": "Armenija",
"Argentina": "Argentina",
"Antigua & Barbuda": "Antigva i Barbuda",
"Antarctica": "Antartika",
"Anguilla": "Angvila",
"Angola": "Angola",
"Andorra": "Andora",
"American Samoa": "Američka Samoa",
"Algeria": "Alžir",
"Albania": "Albanija",
"Åland Islands": "Alandski otoci",
"Afghanistan": "Afganistan",
"United States": "Sjedinjene Države",
"United Kingdom": "Ujedinjeno Kraljevstvo",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Čini se da Vaša email adresa nije povezana s Matrix ID-om na ovom kućnom poslužitelju.",
"This email address was not found": "Ova email adresa nije pronađena",
"Unable to enable Notifications": "Omogućavanje notifikacija nije uspjelo",
"%(brand)s was not given permission to send notifications - please try again": "%(brand)s nema dopuštenje slati Vam notifikacije - molimo pokušajte ponovo",
"%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s nema dopuštenje slati Vam notifikacije - molimo provjerite postavke pretraživača",
"%(name)s is requesting verification": "%(name)s traži potvrdu",
"Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Vaš je kućni poslužitelj odbio vaš pokušaj prijave. Razlog je možda da je jednostavno sve predugo trajalo. Molimo pokušajte ponovno. Ako se ovo nastavi, obratite se administratoru kućnog poslužitelja.",
"Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Vaš kućni poslužitelj nije bio dostupan i nije vas mogao prijaviti. Pokušajte ponovo. Ako se ovo nastavi, obratite se administratoru kućnog poslužitelja.",
"Try again": "Pokušaj ponovo",
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Tražili smo od preglednika da zapamti koji kućni poslužitelj koristite za prijavu, ali ga je Vaš preglednik nažalost zaboravio. Idite na stranicu za prijavu i pokušajte ponovo.",
"We couldn't log you in": "Nismo Vas mogli ulogirati",
"Trust": "Vjeruj",
"Only continue if you trust the owner of the server.": "Nastavite samo ako vjerujete vlasniku poslužitelja.",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Ova radnja zahtijeva pristup zadanom poslužitelju identiteta <server /> radi provjere adrese e-pošte ili telefonskog broja, no poslužitelj nema nikakve uvjete usluge.",
"Identity server has no terms of service": "Poslužitelj identiteta nema uvjete usluge",
"Unnamed Room": "Neimenovana soba",
"Failed to add the following rooms to %(groupId)s:": "Neuspješno dodavanje sljedećih soba u %(groupId)s:",
"Failed to invite users to %(groupId)s": "Neuspješno dodavanje korisnika u %(groupId)s",
"Failed to invite users to community": "Dodavanje korisnika u zajednicu nije uspjelo",
"Failed to invite the following users to %(groupId)s:": "Neuspješno dodavanje sljedećih korisnika u %(groupId)s:",
"Add to community": "Dodaj u zajednicu",
"Room name or address": "Ime ili adresa sobe",
"Add rooms to the community": "Dodaj sobe zajednici",
"Show these rooms to non-members on the community page and room list?": "Prikaži ove sobe osobama koje nisu članovi na stranici zajednice i popisu soba?",
"Which rooms would you like to add to this community?": "Koju sobu biste željeli dodati u ovu zajednicu?",
"Invite to Community": "Pozovi u zajednicu",
"Name or Matrix ID": "Ime ili Matrix ID",
"Invite new community members": "Pozovite nove članove zajednice",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Upozorenje: svaka osoba koju dodate u zajednicu bit će javno vidljiva svima koji znaju ID zajednice",
"Who would you like to add to this community?": "Koga biste željeli dodati u ovu zajednicu?",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s, %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s, %(time)s",
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
"AM": "Prijepodne",
"PM": "Poslijepodne",
"Dec": "Pro",
"Nov": "Stu",
"Oct": "Lis",
"Sep": "Ruj",
"Aug": "Kol",
"Jul": "Srp",
"Jun": "Lip",
"May": "Svi",
"Apr": "Tra",
"Mar": "Ožu",
"Feb": "Velj",
"Jan": "Sij",
"Sat": "Sub",
"Fri": "Pet",
"Thu": "Čet",
"Wed": "Sri",
"Tue": "Uto",
"Mon": "Pon",
"Sun": "Ned",
"Failure to create room": "Stvaranje sobe neuspješno",
"The server does not support the room version specified.": "Poslužitelj ne podržava navedenu verziju sobe.",
"Server may be unavailable, overloaded, or you hit a bug.": "Poslužitelj je možda nedostupan, preopterećen, ili ste pronašli grešku u aplikaciji.",
"Upload Failed": "Prijenos neuspješan",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Datoteka '%(fileName)s' premašuje maksimalnu veličinu ovog kućnog poslužitelja za prijenose",
"The file '%(fileName)s' failed to upload.": "Prijenos datoteke '%(fileName)s' nije uspio.",
"Continue": "Nastavi",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Trenutno nije moguće odgovoriti datotekom. Želite li prenijeti ovu datoteku bez odgovora?",
"Replying With Files": "Odgovaranje datotekama",
"This will end the conference for everyone. Continue?": "To će prekinuti konferenciju za sve. Nastaviti?",
"End conference": "Završi konferenciju",
"You do not have permission to start a conference call in this room": "Nemate dopuštenje uspostaviti konferencijski poziv u ovoj sobi",
"Permission Required": "Potrebno dopuštenje",
"A call is currently being placed!": "Poziv se upravo uspostavlja!",
"Call in Progress": "Poziv u tijeku",
"You cannot place a call with yourself.": "Ne možete uspostaviti poziv sami sa sobom.",
"You're already in a call with this person.": "Već ste u pozivu sa tom osobom.",
"Already in call": "Već u pozivu",
"You've reached the maximum number of simultaneous calls.": "Dosegli ste maksimalan broj istodobnih poziva.",
"Too Many Calls": "Previše poziva",
"You cannot place VoIP calls in this browser.": "Ne možete uspostaviti VoIP pozive u ovom pretraživaču.",
"VoIP is unsupported": "VoIP nije podržan",
"Unable to capture screen": "Nije moguće snimanje zaslona",
"No other application is using the webcam": "Da ni jedna druga aplikacija već ne koristi web kameru",
"Permission is granted to use the webcam": "Jeli dopušteno korištenje web kamere",
"A microphone and webcam are plugged in and set up correctly": "Jesu li mikrofon i web kamera priključeni i pravilno postavljeni",
"Unable to access webcam / microphone": "Nije moguće pristupiti web kameri / mikrofonu",
"Call failed because webcam or microphone could not be accessed. Check that:": "Poziv nije uspio jer nije bilo moguće pristupiti web kameri ili mikrofonu. Provjerite:",
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Poziv nije uspio jer nije bilo moguće pristupiti mikrofonu. Provjerite je li mikrofon priključen i ispravno postavljen.",
"Unable to access microphone": "Nije moguće pristupiti mikrofonu",
"OK": "OK",
"Try using turn.matrix.org": "Pokušajte koristiti turn.matrix.org",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativno, možete pokušati koristiti javni poslužitelj na <code>turn.matrix.org</code>, no to bi moglo biti manje pouzdano i Vaša IP adresa će biti podijeljena s tim poslužiteljem. Time također možete upravljati u Postavkama.",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Zamolite administratora Vašeg kućnog poslužitelja (<code>%(homeserverDomain)s</code>) da konfigurira TURN poslužitelj kako bi pozivi mogli pouzdano funkcionirati.",
"Call failed due to misconfigured server": "Poziv neuspješan radi pogrešno konfiguriranog poslužitelja",
"The call was answered on another device.": "Na poziv je odgovoreno sa drugog uređaja.",
"Answered Elsewhere": "Odgovoreno je drugdje",
"The call could not be established": "Poziv se nije mogao uspostaviti",
"The remote side failed to pick up": "Sugovornik nije odgovorio na poziv",
"The user you called is busy.": "Pozvani korisnik je zauzet.",
"User Busy": "Korisnik zauzet",
"The other party declined the call.": "Sugovornik je odbio poziv.",
"Call Declined": "Poziv odbijen",
"Call Failed": "Poziv neuspješan",
"Unable to load! Check your network connectivity and try again.": "Učitavanje nije moguće! Provjerite mrežnu povezanost i pokušajte ponovo.",
"Error": "Geška",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Gdje ova stranica uključuje identificirajuće podatke, poput ID-a sobe, korisnika ili grupe, ti se podaci uklanjaju prije slanja na poslužitelj.",
"The information being sent to us to help make %(brand)s better includes:": "Podaci koji nam se šalju radi poboljšanja %(brand)s uključuju:",
"Analytics": "Analitika",
"Your device resolution": "Razlučivost vašeg uređaja",
"Your user agent": "Vaš korisnički agent",
"e.g. <CurrentPageURL>": "npr. <CurrentPageURL>",
"Every page you use in the app": "Svaka stranica koju upotrebljavate u aplikaciji",
"Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Bez obzira upotrebljavate li %(brand)s na uređaju na kojem je dodir primarni mehanizam unosa",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Potvrdite dodavanje ovog telefonskog broja koristeći jedinstvenu prijavu (SSO) da biste dokazali Vaš identitet.",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Potvrdite dodavanje ove email adrese koristeći jedinstvenu prijavu (SSO) da biste dokazali Vaš identitet.",
"Single Sign On": "Jedinstvena prijava (SSO)",
"Use Single Sign On to continue": "Koristite jedinstvenu prijavu (SSO) za nastavak",
"Whether or not you're logged in (we don't record your username)": "Bez obzira jeste li ulogirani ili ne (ne snimamo vaše korisničko ime)",
"Add Phone Number": "Dodaj telefonski broj",
"Click the button below to confirm adding this phone number.": "Kliknite gumb ispod da biste potvrdili dodavanje ovog telefonskog broja.",
"Confirm adding phone number": "Potvrdite dodavanje telefonskog broja",
"Add Email Address": "Dodaj email adresu",
"Confirm": "Potvrdi",
"Click the button below to confirm adding this email address.": "Kliknite gumb ispod da biste potvrdili dodavanje ove email adrese.",
"Confirm adding email": "Potvrdite dodavanje email adrese"
} }

View file

@ -26,12 +26,12 @@
"Admin Tools": "Admin. Eszközök", "Admin Tools": "Admin. Eszközök",
"No Microphones detected": "Nem található mikrofon", "No Microphones detected": "Nem található mikrofon",
"No Webcams detected": "Nem található webkamera", "No Webcams detected": "Nem található webkamera",
"No media permissions": "Nincs media jogosultság", "No media permissions": "Nincs média jogosultság",
"You may need to manually permit %(brand)s to access your microphone/webcam": "Lehet hogy kézileg kell engedélyeznie a %(brand)snak, hogy hozzáférjen a mikrofonjához és webkamerájához", "You may need to manually permit %(brand)s to access your microphone/webcam": "Lehet hogy kézileg kell engedélyeznie a %(brand)snak, hogy hozzáférjen a mikrofonjához és webkamerájához",
"Default Device": "Alapértelmezett eszköz", "Default Device": "Alapértelmezett eszköz",
"Microphone": "Mikrofon", "Microphone": "Mikrofon",
"Camera": "Kamera", "Camera": "Kamera",
"Advanced": "Haladó", "Advanced": "Speciális",
"Always show message timestamps": "Üzenet időbélyeg folyamatos megjelenítése", "Always show message timestamps": "Üzenet időbélyeg folyamatos megjelenítése",
"Authentication": "Azonosítás", "Authentication": "Azonosítás",
"Failed to change password. Is your password correct?": "Nem sikerült megváltoztatni a jelszót. Helyesen írtad be a jelszavadat?", "Failed to change password. Is your password correct?": "Nem sikerült megváltoztatni a jelszót. Helyesen írtad be a jelszavadat?",
@ -66,7 +66,7 @@
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s megváltoztatta a hozzáférési szintjét erre: %(powerLevelDiffText)s.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s megváltoztatta a hozzáférési szintjét erre: %(powerLevelDiffText)s.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s megváltoztatta a szoba nevét erre: %(roomName)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s megváltoztatta a szoba nevét erre: %(roomName)s.",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s törölte a szoba nevét.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s törölte a szoba nevét.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s megváltoztatta a témát erre \"%(topic)s\".", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s megváltoztatta a témát erre: „%(topic)s”.",
"Changes your display nickname": "Megváltoztatja a becenevedet", "Changes your display nickname": "Megváltoztatja a becenevedet",
"Click here to fix": "A javításhoz kattints ide", "Click here to fix": "A javításhoz kattints ide",
"Click to mute audio": "Hang némításához kattints ide", "Click to mute audio": "Hang némításához kattints ide",
@ -116,7 +116,7 @@
"Failed to set display name": "Megjelenítési nevet nem sikerült beállítani", "Failed to set display name": "Megjelenítési nevet nem sikerült beállítani",
"Failed to unban": "Kizárás visszavonása sikertelen", "Failed to unban": "Kizárás visszavonása sikertelen",
"Failed to upload profile picture!": "Profil kép feltöltése sikertelen!", "Failed to upload profile picture!": "Profil kép feltöltése sikertelen!",
"Failed to verify email address: make sure you clicked the link in the email": "E-mail cím ellenőrzése sikertelen: ellenőrizze, hogy az e-mailben lévő hivatkozásra kattintott-e", "Failed to verify email address: make sure you clicked the link in the email": "Az e-mail-cím ellenőrzése sikertelen: ellenőrizze, hogy az e-mailben lévő hivatkozásra kattintott-e",
"Failure to create room": "Szoba létrehozása sikertelen", "Failure to create room": "Szoba létrehozása sikertelen",
"Favourites": "Kedvencek", "Favourites": "Kedvencek",
"Fill screen": "Képernyő kitöltése", "Fill screen": "Képernyő kitöltése",
@ -230,7 +230,7 @@
"Submit": "Elküld", "Submit": "Elküld",
"Success": "Sikeres", "Success": "Sikeres",
"The phone number entered looks invalid": "A megadott telefonszám érvénytelennek tűnik", "The phone number entered looks invalid": "A megadott telefonszám érvénytelennek tűnik",
"This email address is already in use": "Ez az e-mail cím már használatban van", "This email address is already in use": "Ez az e-mail-cím már használatban van",
"This email address was not found": "Az e-mail cím nem található", "This email address was not found": "Az e-mail cím nem található",
"The email address linked to your account must be entered.": "A fiókodhoz kötött e-mail címet add meg.", "The email address linked to your account must be entered.": "A fiókodhoz kötött e-mail címet add meg.",
"The remote side failed to pick up": "A hívott fél nem vette fel", "The remote side failed to pick up": "A hívott fél nem vette fel",
@ -1030,7 +1030,7 @@
"Room list": "Szoba lista", "Room list": "Szoba lista",
"Timeline": "Idővonal", "Timeline": "Idővonal",
"Autocomplete delay (ms)": "Automatikus kiegészítés késleltetése (ms)", "Autocomplete delay (ms)": "Automatikus kiegészítés késleltetése (ms)",
"Roles & Permissions": "Szerepek & Jogosultságok", "Roles & Permissions": "Szerepek és jogosultságok",
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "A üzenetek olvashatóságának változtatása csak az új üzenetekre lesz érvényes. A régi üzenetek láthatósága nem fog változni.", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "A üzenetek olvashatóságának változtatása csak az új üzenetekre lesz érvényes. A régi üzenetek láthatósága nem fog változni.",
"Security & Privacy": "Biztonság és adatvédelem", "Security & Privacy": "Biztonság és adatvédelem",
"Encryption": "Titkosítás", "Encryption": "Titkosítás",
@ -1373,7 +1373,7 @@
"Message edits": "Üzenet szerkesztések", "Message edits": "Üzenet szerkesztések",
"Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "A szoba frissítéséhez be kell zárnod ezt a szobát és egy újat kell nyitnod e helyett. A szoba tagjainak a legjobb felhasználói élmény nyújtásához az alábbit fogjuk tenni:", "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "A szoba frissítéséhez be kell zárnod ezt a szobát és egy újat kell nyitnod e helyett. A szoba tagjainak a legjobb felhasználói élmény nyújtásához az alábbit fogjuk tenni:",
"Loading room preview": "Szoba előnézetének a betöltése", "Loading room preview": "Szoba előnézetének a betöltése",
"Show all": "Mindet mutat", "Show all": "Mind megjelenítése",
"%(senderName)s made no change.": "%(senderName)s nem változtatott semmit.", "%(senderName)s made no change.": "%(senderName)s nem változtatott semmit.",
"%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s %(count)s alkalommal nem változtattak semmit", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s %(count)s alkalommal nem változtattak semmit",
"%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s nem változtattak semmit", "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s nem változtattak semmit",
@ -1531,7 +1531,7 @@
"%(count)s unread messages including mentions.|other": "%(count)s olvasatlan üzenet megemlítéssel.", "%(count)s unread messages including mentions.|other": "%(count)s olvasatlan üzenet megemlítéssel.",
"%(count)s unread messages.|other": "%(count)s olvasatlan üzenet.", "%(count)s unread messages.|other": "%(count)s olvasatlan üzenet.",
"Unread mentions.": "Olvasatlan megemlítés.", "Unread mentions.": "Olvasatlan megemlítés.",
"Show image": "Képek megmutatása", "Show image": "Kép megjelenítése",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Ahhoz hogy a hibát megvizsgálhassuk kérlek <newIssueLink>készíts egy új hibajegyet</newIssueLink> a GitHubon.", "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Ahhoz hogy a hibát megvizsgálhassuk kérlek <newIssueLink>készíts egy új hibajegyet</newIssueLink> a GitHubon.",
"To continue you need to accept the terms of this service.": "A folytatáshoz el kell fogadnod a felhasználási feltételeket.", "To continue you need to accept the terms of this service.": "A folytatáshoz el kell fogadnod a felhasználási feltételeket.",
"Document": "Dokumentum", "Document": "Dokumentum",
@ -1545,7 +1545,7 @@
"Remove %(count)s messages|one": "1 üzenet törlése", "Remove %(count)s messages|one": "1 üzenet törlése",
"Your email address hasn't been verified yet": "Az e-mail-címe még nincs ellenőrizve", "Your email address hasn't been verified yet": "Az e-mail-címe még nincs ellenőrizve",
"Click the link in the email you received to verify and then click continue again.": "Ellenőrzéshez kattints a linkre az e-mailben amit kaptál és itt kattints a folytatásra újra.", "Click the link in the email you received to verify and then click continue again.": "Ellenőrzéshez kattints a linkre az e-mailben amit kaptál és itt kattints a folytatásra újra.",
"Add Email Address": "E-mail cím hozzáadása", "Add Email Address": "E-mail-cím hozzáadása",
"Add Phone Number": "Telefonszám hozzáadása", "Add Phone Number": "Telefonszám hozzáadása",
"%(creator)s created and configured the room.": "%(creator)s elkészítette és beállította a szobát.", "%(creator)s created and configured the room.": "%(creator)s elkészítette és beállította a szobát.",
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Először <b>töröld a személyes adatokat</b> az azonosítási szerverről (<idserver />) mielőtt lecsatlakozol. Sajnos az azonosítási szerver (<idserver />) jelenleg elérhetetlen.", "You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Először <b>töröld a személyes adatokat</b> az azonosítási szerverről (<idserver />) mielőtt lecsatlakozol. Sajnos az azonosítási szerver (<idserver />) jelenleg elérhetetlen.",
@ -1622,7 +1622,7 @@
"Subscribing to a ban list will cause you to join it!": "A feliratkozás egy tiltó listára azzal jár, hogy csatlakozol hozzá!", "Subscribing to a ban list will cause you to join it!": "A feliratkozás egy tiltó listára azzal jár, hogy csatlakozol hozzá!",
"If this isn't what you want, please use a different tool to ignore users.": "Ha nem ez az amit szeretnél, kérlek használj más eszközt a felhasználók figyelmen kívül hagyásához.", "If this isn't what you want, please use a different tool to ignore users.": "Ha nem ez az amit szeretnél, kérlek használj más eszközt a felhasználók figyelmen kívül hagyásához.",
"Subscribe": "Feliratkozás", "Subscribe": "Feliratkozás",
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. <a>Mindenképpen megmutat.</a>", "You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. <a>Megjelenítés mindenképpen.</a>",
"Custom (%(level)s)": "Egyéni (%(level)s)", "Custom (%(level)s)": "Egyéni (%(level)s)",
"Trusted": "Megbízható", "Trusted": "Megbízható",
"Not trusted": "Megbízhatatlan", "Not trusted": "Megbízhatatlan",
@ -1733,7 +1733,7 @@
"Country Dropdown": "Ország lenyíló menü", "Country Dropdown": "Ország lenyíló menü",
"The message you are trying to send is too large.": "Túl nagy képet próbálsz elküldeni.", "The message you are trying to send is too large.": "Túl nagy képet próbálsz elküldeni.",
"Help": "Súgó", "Help": "Súgó",
"Show more": "Mutass többet", "Show more": "Több megjelenítése",
"Recent Conversations": "Legújabb Beszélgetések", "Recent Conversations": "Legújabb Beszélgetések",
"Direct Messages": "Közvetlen Beszélgetések", "Direct Messages": "Közvetlen Beszélgetések",
"Go": "Menj", "Go": "Menj",
@ -1795,7 +1795,7 @@
"This bridge was provisioned by <user />.": "Ezt a hidat az alábbi felhasználó készítette: <user />.", "This bridge was provisioned by <user />.": "Ezt a hidat az alábbi felhasználó készítette: <user />.",
"Workspace: %(networkName)s": "Munkahely: %(networkName)s", "Workspace: %(networkName)s": "Munkahely: %(networkName)s",
"Channel: %(channelName)s": "Csatorna: %(channelName)s", "Channel: %(channelName)s": "Csatorna: %(channelName)s",
"Show less": "Kevesebbet mutat", "Show less": "Kevesebb megjelenítése",
"Securely cache encrypted messages locally for them to appear in search results, using ": "A titkosított üzenetek kereséséhez azokat biztonságos módon helyileg kell tárolnod, felhasználva: ", "Securely cache encrypted messages locally for them to appear in search results, using ": "A titkosított üzenetek kereséséhez azokat biztonságos módon helyileg kell tárolnod, felhasználva: ",
" to store messages from ": " üzenetek tárolásához ", " to store messages from ": " üzenetek tárolásához ",
"rooms.": "szobából.", "rooms.": "szobából.",
@ -2109,7 +2109,7 @@
"Server did not return valid authentication information.": "A szerver semmilyen érvényes azonosítási információt sem küldött vissza.", "Server did not return valid authentication information.": "A szerver semmilyen érvényes azonosítási információt sem küldött vissza.",
"There was a problem communicating with the server. Please try again.": "A szerverrel való kommunikációval probléma történt. Kérlek próbáld újra.", "There was a problem communicating with the server. Please try again.": "A szerverrel való kommunikációval probléma történt. Kérlek próbáld újra.",
"Sign in with SSO": "Belépés SSO-val", "Sign in with SSO": "Belépés SSO-val",
"Welcome to %(appName)s": "Üdvözöl az %(appName)s", "Welcome to %(appName)s": "Üdvözöl a(z) %(appName)s",
"Liberate your communication": "Kommunikálj szabadon", "Liberate your communication": "Kommunikálj szabadon",
"Send a Direct Message": "Közvetlen üzenet küldése", "Send a Direct Message": "Közvetlen üzenet küldése",
"Explore Public Rooms": "Nyilvános szobák felfedezése", "Explore Public Rooms": "Nyilvános szobák felfedezése",
@ -2237,7 +2237,7 @@
"Sort by": "Rendezés", "Sort by": "Rendezés",
"Unread rooms": "Olvasatlan szobák", "Unread rooms": "Olvasatlan szobák",
"Always show first": "Mindig az elsőt mutatja", "Always show first": "Mindig az elsőt mutatja",
"Show": "Mutat", "Show": "Megjelenítés",
"Message preview": "Üzenet előnézet", "Message preview": "Üzenet előnézet",
"List options": "Lista beállításai", "List options": "Lista beállításai",
"Show %(count)s more|other": "Még %(count)s megjelenítése", "Show %(count)s more|other": "Még %(count)s megjelenítése",
@ -2945,7 +2945,7 @@
"Learn more": "Tudj meg többet", "Learn more": "Tudj meg többet",
"Matrix.org is the biggest public homeserver in the world, so its a good place for many.": "A matrix.org a legnagyobb nyilvános Matrix szerver a világon, és sok felhasználónak megfelelő választás.", "Matrix.org is the biggest public homeserver in the world, so its a good place for many.": "A matrix.org a legnagyobb nyilvános Matrix szerver a világon, és sok felhasználónak megfelelő választás.",
"About homeservers": "A Matrix szerverekről", "About homeservers": "A Matrix szerverekről",
"Use your preferred Matrix homeserver if you have one, or host your own.": "Add meg az általad választott Matrix szerver címét, ha van ilyen, vagy üzemeltess egy sajátot!", "Use your preferred Matrix homeserver if you have one, or host your own.": "Add meg az általad választott Matrix szerver címét, ha van ilyen, vagy üzemeltess egy sajátot.",
"Other homeserver": "Másik Matrix szerver", "Other homeserver": "Másik Matrix szerver",
"Host account on": "Fiók létrehozása itt:", "Host account on": "Fiók létrehozása itt:",
"We call the places where you can host your account homeservers.": "Matrix szervereknek nevezzük azokat a helyeket, ahol fiókot lehet létrehozni.", "We call the places where you can host your account homeservers.": "Matrix szervereknek nevezzük azokat a helyeket, ahol fiókot lehet létrehozni.",
@ -3107,7 +3107,7 @@
"Room name": "Szoba neve", "Room name": "Szoba neve",
"Support": "Támogatás", "Support": "Támogatás",
"Random": "Véletlen", "Random": "Véletlen",
"Welcome to <name/>": "Üdvözlöm itt: <name/>", "Welcome to <name/>": "Üdvözöl a(z) <name/>",
"Your private space <name/>": "Privát tere: <name/>", "Your private space <name/>": "Privát tere: <name/>",
"Your public space <name/>": "Nyilvános tere: <name/>", "Your public space <name/>": "Nyilvános tere: <name/>",
"You have been invited to <name/>": "Meghívták ide: <name/>", "You have been invited to <name/>": "Meghívták ide: <name/>",
@ -3370,5 +3370,11 @@
"See when people join, leave, or are invited to your active room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése az aktív szobájában", "See when people join, leave, or are invited to your active room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése az aktív szobájában",
"Kick, ban, or invite people to your active room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket az aktív szobába és, hogy ön elhagyja a szobát", "Kick, ban, or invite people to your active room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket az aktív szobába és, hogy ön elhagyja a szobát",
"See when people join, leave, or are invited to this room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése ebben a szobában", "See when people join, leave, or are invited to this room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése ebben a szobában",
"Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát" "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát",
"Currently joining %(count)s rooms|one": "%(count)s szobába lép be",
"Currently joining %(count)s rooms|other": "%(count)s szobába lép be",
"No results for \"%(query)s\"": "Nincs találat ehhez: %(query)s",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Próbáljon ki más szavakat vagy keressen elgépelést. Néhány találat azért nem látszik, mert privát és meghívóra van szüksége, hogy csatlakozhasson.",
"The user you called is busy.": "A hívott felhasználó foglalt.",
"User Busy": "Felhasználó foglalt"
} }

View file

@ -720,5 +720,13 @@
"%(count)s messages deleted.|one": "%(count)s skilaboð eytt.", "%(count)s messages deleted.|one": "%(count)s skilaboð eytt.",
"%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.", "%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.",
"Message deleted on %(date)s": "Skilaboð eytt á %(date)s", "Message deleted on %(date)s": "Skilaboð eytt á %(date)s",
"Message edits": "Skilaboðs breytingar" "Message edits": "Skilaboðs breytingar",
"List options": "Lista valkosti",
"Create a Group Chat": "Búa Til Hópspjall",
"Explore Public Rooms": "Kanna Almenningsherbergi",
"Explore public rooms": "Kanna almenningsherbergi",
"Explore all public rooms": "Kanna öll almenningsherbergi",
"Liberate your communication": "Frelsaðu samskipti þín",
"Welcome to <name/>": "Velkomin til <name/>",
"Welcome to %(appName)s": "Velkomin til %(appName)s"
} }

View file

@ -3375,5 +3375,11 @@
"Kick, ban, or invite people to your active room, and make you leave": "Buttare fuori, bandire o invitare persone nella tua stanza attiva e farti uscire", "Kick, ban, or invite people to your active room, and make you leave": "Buttare fuori, bandire o invitare persone nella tua stanza attiva e farti uscire",
"See when people join, leave, or are invited to this room": "Vedere quando le persone entrano, escono o sono invitate in questa stanza", "See when people join, leave, or are invited to this room": "Vedere quando le persone entrano, escono o sono invitate in questa stanza",
"Kick, ban, or invite people to this room, and make you leave": "Buttare fuori, bandire o invitare persone in questa stanza e farti uscire", "Kick, ban, or invite people to this room, and make you leave": "Buttare fuori, bandire o invitare persone in questa stanza e farti uscire",
"See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva" "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva",
"Currently joining %(count)s rooms|one": "Stai entrando in %(count)s stanza",
"Currently joining %(count)s rooms|other": "Stai entrando in %(count)s stanze",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prova parole diverse o controlla errori di battitura. Alcuni risultati potrebbero non essere visibili dato che sono privati e ti servirebbe un invito per unirti.",
"No results for \"%(query)s\"": "Nessun risultato per \"%(query)s\"",
"The user you called is busy.": "L'utente che hai chiamato è occupato.",
"User Busy": "Utente occupato"
} }

View file

@ -3261,5 +3261,11 @@
"See when people join, leave, or are invited to your active room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd in uw actieve gesprek", "See when people join, leave, or are invited to your active room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd in uw actieve gesprek",
"Kick, ban, or invite people to your active room, and make you leave": "Verwijder, verban of nodig personen uit voor uw actieve gesprek en uzelf laten vertrekken", "Kick, ban, or invite people to your active room, and make you leave": "Verwijder, verban of nodig personen uit voor uw actieve gesprek en uzelf laten vertrekken",
"See when people join, leave, or are invited to this room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd voor dit gesprek", "See when people join, leave, or are invited to this room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd voor dit gesprek",
"Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken" "Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken",
"Currently joining %(count)s rooms|one": "Momenteel aan het toetreden tot %(count)s gesprek",
"Currently joining %(count)s rooms|other": "Momenteel aan het toetreden tot %(count)s gesprekken",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Probeer andere woorden of controleer op typefouten. Sommige resultaten zijn mogelijk niet zichtbaar omdat ze privé zijn of u een uitnodiging nodig heeft om deel te nemen.",
"No results for \"%(query)s\"": "Geen resultaten voor \"%(query)s\"",
"The user you called is busy.": "De gebruiker die u belde is bezet.",
"User Busy": "Gebruiker Bezet"
} }

View file

@ -2271,5 +2271,86 @@
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Poprosiliśmy przeglądarkę o zapamiętanie, z którego serwera głównego korzystasz, aby umożliwić Ci logowanie, ale niestety Twoja przeglądarka o tym zapomniała. Przejdź do strony logowania i spróbuj ponownie.", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Poprosiliśmy przeglądarkę o zapamiętanie, z którego serwera głównego korzystasz, aby umożliwić Ci logowanie, ale niestety Twoja przeglądarka o tym zapomniała. Przejdź do strony logowania i spróbuj ponownie.",
"We couldn't log you in": "Nie mogliśmy Cię zalogować", "We couldn't log you in": "Nie mogliśmy Cię zalogować",
"You're already in a call with this person.": "Prowadzisz już rozmowę z tą osobą.", "You're already in a call with this person.": "Prowadzisz już rozmowę z tą osobą.",
"Already in call": "Już dzwoni" "Already in call": "Już dzwoni",
"Never send encrypted messages to unverified sessions in this room from this session": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych sesji z tej sesji w tym pokoju",
"Use the <a>Desktop app</a> to see all encrypted files": "Użyj <a>aplikacji desktopowej</a>, aby zobaczyć wszystkie szyfrowane pliki",
"Youre all caught up": "Jesteś na bieżąco",
"Verification requested": "Zażądano weryfikacji",
"You have no visible notifications.": "Nie masz widocznych powiadomień.",
"Connecting": "Łączenie",
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Twój klucz bezpieczeństwa został <b>skopiowany do schowka</b>, wklej go:",
"Your Security Key": "Twój klucz bezpieczeństwa",
"Create key backup": "Utwórz kopię zapasową klucza",
"Generate a Security Key": "Wygeneruj klucz bezpieczeństwa",
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Zabezpiecz się przed utratą dostępu do szyfrowanych wiadomości i danych, tworząc kopię zapasową kluczy szyfrowania na naszym serwerze.",
"Safeguard against losing access to encrypted messages & data": "Zabezpiecz się przed utratą dostępu do szyfrowanych wiadomości i danych",
"Access Token": "Token dostępu",
"Your access token gives full access to your account. Do not share it with anyone.": "Twój token dostępu daje pełen dostęp do Twojego konta. Nie dziel się nim z nikim.",
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Możesz opuścić betę w każdej chwili z ustawień lub klikając plakietę Beta, taką jak powyższa.",
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta dostępna w przeglądarce, na komputerach i na Androidzie. Niektóre funkcje mogą nie być dostępne na Twoim serwerze.",
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta dostępna w przeglądarce, na komputerach i na Androidzie. Dziękujemy za wypróbowanie bety.",
"Avatar": "Awatar",
"Leave the beta": "Opuść betę",
"Beta": "Beta",
"Tap for more info": "Naciśnij aby dowiedzieć się więcej",
"Move right": "Przenieś w prawo",
"Move left": "Przenieś w lewo",
"Join the beta": "Dołącz do bety",
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "Aby dołączyć do %(spaceName)s, włącz <a>betę Przestrzeni</a>",
"No results found": "Nie znaleziono wyników",
"Create room": "Utwórz pokój",
"Removing...": "Usuwanie…",
"Your server does not support showing space hierarchies.": "Twój serwer nie obsługuje wyświetlania hierarchii przestrzeni.",
"You can select all or individual messages to retry or delete": "Możesz zaznaczyć wszystkie lub wybrane wiadomości aby spróbować ponownie lub usunąć je",
"Sending": "Wysyłanie",
"Delete all": "Usuń wszystkie",
"Some of your messages have not been sent": "Niektóre z Twoich wiadomości nie zostały wysłane",
"Filter rooms and people": "Filtruj pokoje i ludzi",
"No results for \"%(query)s\"": "Brak wyników dla „%(query)s”",
"Explore rooms in %(communityName)s": "Eksploruj pokoje w %(communityName)s",
"Retry all": "Spróbuj ponownie wszystkie",
"Suggested": "Polecany",
"This room is suggested as a good one to join": "Ten pokój jest polecany jako dobry do dołączenia",
"%(count)s rooms|one": "%(count)s pokój",
"%(count)s rooms|other": "%(count)s pokoi",
"%(count)s members|one": "%(count)s członek",
"%(count)s members|other": "%(count)s członkowie",
"You don't have permission": "Nie masz uprawnień",
"Failed to remove some rooms. Try again later": "Nie udało się usunąć niektórych pokoi. Spróbuj ponownie później",
"Select a room below first": "Najpierw wybierz poniższy pokój",
"%(count)s rooms and 1 space|one": "%(count)s pokój i jedna przestrzeń",
"%(count)s rooms and 1 space|other": "%(count)s pokoi i jedna przestrzeń",
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "Aby wyświetlić %(spaceName)s, włącz <a>betę Przestrzeni</a>",
"Spaces are a beta feature.": "Przestrzenie są funkcją beta.",
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s pokój i %(numSpaces)s przestrzeni",
"%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s pokoi i %(numSpaces)s przestrzeni",
"Filter all spaces": "Filtruj wszystkie przestrzenie",
"Communities are changing to Spaces": "Społeczności zmieniają się na Przestrzenie",
"Spaces is a beta feature": "Przestrzenie są funkcją beta",
"You can add existing spaces to a space.": "Możesz dodać istniejące przestrzenie do przestrzeni.",
"Filter your rooms and spaces": "Filtruj pokoje i przestrzenie",
"%(count)s results in all spaces|one": "%(count)s wynik we wszystkich przestrzeniach",
"%(count)s results in all spaces|other": "%(count)s wyników we wszystkich przestrzeniach",
"Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Przestrzenie to nowy sposób na grupowanie pokoi i ludzi. Aby dołączyć do istniejącej przestrzeni, potrzebujesz zaproszenia.",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Twoje opinie pomogą uczynić Przestrzenie lepszymi. Im bardziej szczegółowo je opiszesz, tym lepiej.",
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s odświeży się z włączonymi Przestrzeniami. Społeczności i niestandardowe tagi zostaną ukryte.",
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Jeżeli opuścisz, %(brand)s odświeży się z wyłączonymi Przestrzeniami. Społeczności i niestandardowe tagi będą z powrotem widoczne.",
"Spaces are a new way to group rooms and people.": "Przestrzenie to nowy sposób grupowania pokoi i ludzi.",
"Spaces": "Przestrzenie",
"Feeling experimental?": "Chcesz eksperymentować?",
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Chcesz eksperymentować? Laboratoria to najlepszy sposób na uzyskanie nowości wcześniej, przetestowanie nowych funkcji i pomoc w kształtowaniu ich zanim będą ogólnodostępne. <a>Dowiedz się więcej</a>.",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Będziemy przechowywać zaszyfrowaną kopię Twoich kluczy na naszym serwerze. Zabezpiecz swoją kopię zapasową frazą bezpieczeństwa.",
"Secure Backup": "Bezpieczna kopia zapasowa",
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Pozwól na wykorzystanie peer-to-peer w rozmowach 1:1 (jeżeli włączono, druga strona może zobaczyć Twój adres IP)",
"Jump to the bottom of the timeline when you send a message": "Przejdź na dół osi czasu po wysłaniu wiadomości",
"Use Ctrl + F to search": "Używaj Ctrl + F do wyszukiwania",
"Show line numbers in code blocks": "Pokazuj numery wierszy w blokach kodu",
"Expand code blocks by default": "Domyślnie rozwijaj bloki kodu",
"Show stickers button": "Pokaż przycisk naklejek",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Zarządcy integracji otrzymują dane konfiguracji, mogą modyfikować widżety, wysyłać zaproszenia do pokoi i ustawiać poziom uprawnień w Twoim imieniu.",
"Converts the DM to a room": "Zmienia wiadomości bezpośrednie w pokój",
"Converts the room to a DM": "Zmienia pokój w wiadomość bezpośrednią",
"Sends the given message as a spoiler": "Wysyła podaną wiadomość jako spoiler",
"User Busy": "Użytkownik zajęty",
"The user you called is busy.": "Użytkownik do którego zadzwoniłeś(-aś) jest zajęty."
} }

View file

@ -163,7 +163,7 @@
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s enviou um convite para %(targetDisplayName)s entrar na sala.", "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s enviou um convite para %(targetDisplayName)s entrar na sala.",
"%(senderName)s set a profile picture.": "%(senderName)s definiu uma foto de perfil.", "%(senderName)s set a profile picture.": "%(senderName)s definiu uma foto de perfil.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s definiu o nome e sobrenome para %(displayName)s.", "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s definiu o nome e sobrenome para %(displayName)s.",
"This email address is already in use": "Este endereço de e-mail já está em uso", "This email address is already in use": "Este endereço de email já está em uso",
"This email address was not found": "Este endereço de e-mail não foi encontrado", "This email address was not found": "Este endereço de e-mail não foi encontrado",
"The remote side failed to pick up": "A pessoa não atendeu a chamada", "The remote side failed to pick up": "A pessoa não atendeu a chamada",
"This room is not recognised.": "Esta sala não é reconhecida.", "This room is not recognised.": "Esta sala não é reconhecida.",
@ -284,7 +284,7 @@
"ex. @bob:example.com": "p.ex: @joao:exemplo.com", "ex. @bob:example.com": "p.ex: @joao:exemplo.com",
"Add User": "Adicionar usuária(o)", "Add User": "Adicionar usuária(o)",
"Custom Server Options": "Opções para Servidor Personalizado", "Custom Server Options": "Opções para Servidor Personalizado",
"Dismiss": "Descartar", "Dismiss": "Dispensar",
"Please check your email to continue registration.": "Por favor, confirme o seu e-mail para continuar a inscrição.", "Please check your email to continue registration.": "Por favor, confirme o seu e-mail para continuar a inscrição.",
"Token incorrect": "Token incorreto", "Token incorrect": "Token incorreto",
"Please enter the code it contains:": "Por favor, entre com o código que está na mensagem:", "Please enter the code it contains:": "Por favor, entre com o código que está na mensagem:",
@ -1176,7 +1176,7 @@
"Sign In or Create Account": "Faça login ou crie uma conta", "Sign In or Create Account": "Faça login ou crie uma conta",
"Use your account or create a new one to continue.": "Use sua conta ou crie uma nova para continuar.", "Use your account or create a new one to continue.": "Use sua conta ou crie uma nova para continuar.",
"Create Account": "Criar Conta", "Create Account": "Criar Conta",
"Sign In": "Entrar", "Sign In": "Fazer signin",
"Custom (%(level)s)": "Personalizado (%(level)s)", "Custom (%(level)s)": "Personalizado (%(level)s)",
"Messages": "Mensagens", "Messages": "Mensagens",
"Actions": "Ações", "Actions": "Ações",

View file

@ -3361,5 +3361,14 @@
"sends space invaders": "dërgon pushtues hapësire", "sends space invaders": "dërgon pushtues hapësire",
"Sends the given message with a space themed effect": "E dërgon mesazhin e dhënë me një efekt teme hapësinore", "Sends the given message with a space themed effect": "E dërgon mesazhin e dhënë me një efekt teme hapësinore",
"See when people join, leave, or are invited to your active room": "Shihni kur persona vijnë, ikin ose janë ftuar në dhomën tuaj aktive", "See when people join, leave, or are invited to your active room": "Shihni kur persona vijnë, ikin ose janë ftuar në dhomën tuaj aktive",
"See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë" "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë",
"Space Autocomplete": "Vetëplotësim Hapësire",
"Currently joining %(count)s rooms|one": "Aktualisht duke hyrë në %(count)s dhomë",
"Currently joining %(count)s rooms|other": "Aktualisht duke hyrë në %(count)s dhoma",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provoni fjalë të ndryshme, ose kontrolloni për gabime shkrimi. Disa përfundime mund të mos jenë të dukshme, ngaqë janë private dhe ju duhet një ftesë për të marrë pjesë në to.",
"No results for \"%(query)s\"": "Ska përfundime për \"%(query)s\"",
"The user you called is busy.": "Përdoruesi që thirrët është i zënë.",
"User Busy": "Përdoruesi Është i Zënë",
"Kick, ban, or invite people to your active room, and make you leave": "Përzini, dëboni, ose ftoni persona te dhoma juaj aktive, dhe bëni largimin tuaj",
"Kick, ban, or invite people to this room, and make you leave": "Përzini, dëboni, ose ftoni persona në këtë dhomë, dhe bëni largimin tuaj"
} }

View file

@ -3305,5 +3305,11 @@
"See when people join, leave, or are invited to your active room": "Se när folk går med, lämnar eller bjuds in till ditt aktiva rum", "See when people join, leave, or are invited to your active room": "Se när folk går med, lämnar eller bjuds in till ditt aktiva rum",
"Kick, ban, or invite people to your active room, and make you leave": "Kicka, banna eller bjuda in folk till ditt aktiva rum, och tvinga dig att lämna", "Kick, ban, or invite people to your active room, and make you leave": "Kicka, banna eller bjuda in folk till ditt aktiva rum, och tvinga dig att lämna",
"See when people join, leave, or are invited to this room": "Se när folk går med, lämnar eller bjuds in till det här rummet", "See when people join, leave, or are invited to this room": "Se när folk går med, lämnar eller bjuds in till det här rummet",
"Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna" "Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna",
"Currently joining %(count)s rooms|one": "Går just nu med i %(count)s rum",
"Currently joining %(count)s rooms|other": "Går just nu med i %(count)s rum",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Testa andra ord eller kolla efter felskrivningar. Vissa resultat kanske inte visas för att de är privata och du behöver en inbjudan för att gå med i dem.",
"No results for \"%(query)s\"": "Inga resultat för \"%(query)s\"",
"The user you called is busy.": "Användaren du ringde är upptagen.",
"User Busy": "Användare upptagen"
} }

View file

@ -267,8 +267,8 @@
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s",
"Who would you like to add to this community?": "Кого ви хочете додати до цієї спільноти?", "Who would you like to add to this community?": "Кого ви хочете додати до цієї спільноти?",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Там, де ця сторінка містить ототожненну інформацію, як-от назва кімнати, користувача чи групи, ці дані будуть вилучені перед надсиланням на сервер.", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Там, де ця сторінка містить ототожненну інформацію, як-от назва кімнати, користувача чи групи, ці дані будуть вилучені перед надсиланням на сервер.",
"Call in Progress": "Іде виклик", "Call in Progress": "Триває виклик",
"A call is currently being placed!": "Зараз іде виклик!", "A call is currently being placed!": "Зараз триває виклик!",
"A call is already in progress!": "Вже здійснюється дзвінок!", "A call is already in progress!": "Вже здійснюється дзвінок!",
"Permission Required": "Потрібен дозвіл", "Permission Required": "Потрібен дозвіл",
"You do not have permission to start a conference call in this room": "У вас немає дозволу, щоб розпочати дзвінок-конференцію в цій кімнаті", "You do not have permission to start a conference call in this room": "У вас немає дозволу, щоб розпочати дзвінок-конференцію в цій кімнаті",
@ -306,8 +306,8 @@
"Missing user_id in request": "У запиті пропущено user_id", "Missing user_id in request": "У запиті пропущено user_id",
"Usage": "Використання", "Usage": "Використання",
"Searches DuckDuckGo for results": "Здійснює пошук через DuckDuckGo", "Searches DuckDuckGo for results": "Здійснює пошук через DuckDuckGo",
"/ddg is not a command": "/ddg — це не команда", "/ddg is not a command": "/ddg не є командою",
"To use it, just wait for autocomplete results to load and tab through them.": "Щоб цим скористатися, просто почекайте на підказки доповнення й перемикайтеся між ними клавішею TAB.", "To use it, just wait for autocomplete results to load and tab through them.": "Щоб цим скористатися, просто почекайте на підказки автодоповнення й перемикайтеся між ними клавішею TAB.",
"Changes your display nickname": "Змінює ваш нік", "Changes your display nickname": "Змінює ваш нік",
"Invites user with given id to current room": "Запрошує користувача з вказаним ідентифікатором до кімнати", "Invites user with given id to current room": "Запрошує користувача з вказаним ідентифікатором до кімнати",
"Leave room": "Залишити кімнату", "Leave room": "Залишити кімнату",
@ -457,7 +457,7 @@
"Actions": "Дії", "Actions": "Дії",
"Other": "Інше", "Other": "Інше",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Додає ¯\\_(ツ)_/¯ на початку текстового повідомлення", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Додає ¯\\_(ツ)_/¯ на початку текстового повідомлення",
"Sends a message as plain text, without interpreting it as markdown": "Надсилає повідомлення як чистий текст, не використовуючи markdown", "Sends a message as plain text, without interpreting it as markdown": "Надсилає повідомлення у вигляді звичайного тексту, не інтерпретуючи його як розмітку",
"Upgrades a room to a new version": "Покращує кімнату до нової версії", "Upgrades a room to a new version": "Покращує кімнату до нової версії",
"You do not have the required permissions to use this command.": "Вам бракує дозволу на використання цієї команди.", "You do not have the required permissions to use this command.": "Вам бракує дозволу на використання цієї команди.",
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Увага!</b>: Поліпшення кімнати <i>не перенесе автоматично усіх учасників до нової версії кімнати.</i> Ми опублікуємо посилання на нову кімнату у старій версії кімнати, а учасники мають власноруч клацнути це посилання, щоб приєднатися до нової кімнати.", "<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Увага!</b>: Поліпшення кімнати <i>не перенесе автоматично усіх учасників до нової версії кімнати.</i> Ми опублікуємо посилання на нову кімнату у старій версії кімнати, а учасники мають власноруч клацнути це посилання, щоб приєднатися до нової кімнати.",
@ -624,7 +624,7 @@
"Riot is now Element!": "Riot тепер - Element!", "Riot is now Element!": "Riot тепер - Element!",
"Learn More": "Дізнатися більше", "Learn More": "Дізнатися більше",
"Command error": "Помилка команди", "Command error": "Помилка команди",
"Sends a message as html, without interpreting it as markdown": "Надсилає повідомлення як HTML, не інтерпритуючи Markdown", "Sends a message as html, without interpreting it as markdown": "Надсилає повідомлення у вигляді HTML, не інтерпретуючи його як розмітку",
"Failed to set topic": "Не вдалося встановити тему", "Failed to set topic": "Не вдалося встановити тему",
"Once enabled, encryption cannot be disabled.": "Після увімкнення шифрування не можна буде вимкнути.", "Once enabled, encryption cannot be disabled.": "Після увімкнення шифрування не можна буде вимкнути.",
"Please enter verification code sent via text.": "Будь ласка, введіть звірювальний код, відправлений у текстовому повідомленні.", "Please enter verification code sent via text.": "Будь ласка, введіть звірювальний код, відправлений у текстовому повідомленні.",
@ -1602,5 +1602,33 @@
"Share Link to User": "Поділитися посиланням на користувача", "Share Link to User": "Поділитися посиланням на користувача",
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Повідомлення тут захищено наскрізним шифруванням. Підтвердьте %(displayName)s у їхньому профілі — натиснувши на їх аватар.", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Повідомлення тут захищено наскрізним шифруванням. Підтвердьте %(displayName)s у їхньому профілі — натиснувши на їх аватар.",
"Open": "Відкрити", "Open": "Відкрити",
"<a>In reply to</a> <pill>": "<a>У відповідь на</a> <pill>" "<a>In reply to</a> <pill>": "<a>У відповідь на</a> <pill>",
"The user you called is busy.": "Користувач, якого ви викликаєте, зайнятий.",
"User Busy": "Користувач зайнятий",
"Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message": "Додає ┬──┬ ( ゜-゜ノ) на початку текстового повідомлення",
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Додає (╯°□°)╯︵ ┻━┻ на початку текстового повідомлення",
"We couldn't log you in": "Нам не вдалося виконати вхід",
"You're already in a call with this person.": "Ви вже спілкуєтесь із цією особою.",
"Already in call": "Вже у виклику",
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Ви не можете надсилати жодних повідомлень, поки не переглянете та не погодитесь з <consentLink>нашими умовами та положеннями</consentLink>.",
"Send message": "Надіслати повідомлення",
"Sending your message...": "Надсилання повідомлення...",
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "Ви можете скористатися <code>/help</code> для перегляду доступних команд. Ви мали намір надіслати це як повідомлення?",
"Send messages": "Надіслати повідомлення",
"sends confetti": "надсилає конфеті",
"sends fireworks": "надсилає феєрверк",
"sends space invaders": "надсилає тему про космічних загарбників",
"Sends the given message with a space themed effect": "Надсилає це повідомлення з космічними ефектами",
"unknown person": "невідома особа",
"Sends the given message with snowfall": "Надсилає це повідомлення зі снігопадом",
"Sends the given message with fireworks": "Надсилає це повідомлення з феєрверком",
"Sends the given message with confetti": "Надсилає це повідомлення з конфеті",
"Use Ctrl + Enter to send a message": "Натисніть Ctrl + Enter, щоб надіслати повідомлення",
"Use Command + Enter to send a message": "Натисніть Command + Enter, щоб надіслати повідомлення",
"Use Ctrl + F to search": "Натисніть Ctrl + F, щоб шукати",
"Use Command + F to search": "Натисніть Command + F, щоб шукати",
"Send text messages as you in this room": "Надіслати текстові повідомлення у цю кімнату від свого імені",
"Send messages as you in your active room": "Надіслати повідомлення у свою активну кімнату від свого імені",
"Send messages as you in this room": "Надіслати повідомлення у цю кімнату від свого імені",
"Sends the given message as a spoiler": "Надсилає вказане повідомлення згорненим"
} }

View file

@ -3274,5 +3274,11 @@
"Send stickers to your active room as you": "发送贴纸到你所活跃的聊天室", "Send stickers to your active room as you": "发送贴纸到你所活跃的聊天室",
"See when people join, leave, or are invited to your active room": "查看人们何时加入、离开或被邀请到你所活跃的聊天室", "See when people join, leave, or are invited to your active room": "查看人们何时加入、离开或被邀请到你所活跃的聊天室",
"See when people join, leave, or are invited to this room": "查看人们何时加入、离开或被邀请到这个房间", "See when people join, leave, or are invited to this room": "查看人们何时加入、离开或被邀请到这个房间",
"Kick, ban, or invite people to this room, and make you leave": "移除、封禁或邀请用户到此聊天室,并让你离开" "Kick, ban, or invite people to this room, and make you leave": "移除、封禁或邀请用户到此聊天室,并让你离开",
"Currently joining %(count)s rooms|one": "目前正在加入 %(count)s 个聊天室",
"Currently joining %(count)s rooms|other": "目前正在加入 %(count)s 个聊天室",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "尝试不同的单词或检查拼写错误。某些结果可能不可见,因为它们属于私有的,你需要一个邀请才能加入。",
"No results for \"%(query)s\"": "「%(query)s」没有结果",
"The user you called is busy.": "你所拨打的用户正在忙碌中。",
"User Busy": "用户正在忙"
} }

View file

@ -3378,5 +3378,11 @@
"See when people join, leave, or are invited to your active room": "檢視人們何時加入、離開或被邀請至您活躍的聊天室", "See when people join, leave, or are invited to your active room": "檢視人們何時加入、離開或被邀請至您活躍的聊天室",
"Kick, ban, or invite people to your active room, and make you leave": "踢除、封鎖或邀請人們到您作用中的聊天室,然後讓您離開", "Kick, ban, or invite people to your active room, and make you leave": "踢除、封鎖或邀請人們到您作用中的聊天室,然後讓您離開",
"See when people join, leave, or are invited to this room": "檢視人們何時加入、離開或被邀請至此聊天室", "See when people join, leave, or are invited to this room": "檢視人們何時加入、離開或被邀請至此聊天室",
"Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開" "Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開",
"Currently joining %(count)s rooms|one": "目前正在加入 %(count)s 個聊天室",
"Currently joining %(count)s rooms|other": "目前正在加入 %(count)s 個聊天室",
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "嘗試不同的詞或是檢查拼字。某些結果可能不可見,因為其為私人的,您必須要有邀請才能加入。",
"No results for \"%(query)s\"": "「%(query)s」沒有結果",
"The user you called is busy.": "您想要通話的使用者目前忙碌中。",
"User Busy": "使用者忙碌"
} }

View file

@ -20,6 +20,7 @@ import SettingsStore from "../settings/SettingsStore";
import {_t} from "../languageHandler"; import {_t} from "../languageHandler";
import dis from "../dispatcher/dispatcher"; import dis from "../dispatcher/dispatcher";
import {SettingLevel} from "../settings/SettingLevel"; import {SettingLevel} from "../settings/SettingLevel";
import { Preset } from "matrix-js-sdk/src/@types/partials";
// TODO: Move this and related files to the js-sdk or something once finalized. // TODO: Move this and related files to the js-sdk or something once finalized.
@ -86,7 +87,7 @@ export class Mjolnir {
const resp = await MatrixClientPeg.get().createRoom({ const resp = await MatrixClientPeg.get().createRoom({
name: _t("My Ban List"), name: _t("My Ban List"),
topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), topic: _t("This is your list of users/servers you have blocked - don't leave the room!"),
preset: "private_chat", preset: Preset.PrivateChat,
}); });
personalRoomId = resp['room_id']; personalRoomId = resp['room_id'];
await SettingsStore.setValue( await SettingsStore.setValue(

View file

@ -25,14 +25,8 @@ import Tar from "tar-js";
import * as rageshake from './rageshake'; import * as rageshake from './rageshake';
// polyfill textencoder if necessary
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import SdkConfig from "../SdkConfig"; import SdkConfig from "../SdkConfig";
let TextEncoder = window.TextEncoder;
if (!TextEncoder) {
TextEncoder = TextEncodingUtf8.TextEncoder;
}
interface IOpts { interface IOpts {
label?: string; label?: string;

View file

@ -17,6 +17,7 @@
import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import {IState} from "./components/structures/RoomView";
interface IDiff { interface IDiff {
isMemberEvent: boolean; isMemberEvent: boolean;
@ -47,11 +48,18 @@ function memberEventDiff(ev: MatrixEvent): IDiff {
return diff; return diff;
} }
export default function shouldHideEvent(ev: MatrixEvent): boolean { /**
// Wrap getValue() for readability. Calling the SettingsStore can be * Determines whether the given event should be hidden from timelines.
// fairly resource heavy, so the checks below should avoid hitting it * @param ev The event
// where possible. * @param ctx An optional RoomContext to pull cached settings values from to avoid
const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); * hitting the settings store
*/
export default function shouldHideEvent(ev: MatrixEvent, ctx?: IState): boolean {
// Accessing the settings store directly can be expensive if done frequently,
// so we should prefer using cached values if a RoomContext is available
const isEnabled = ctx ?
name => ctx[name] :
name => SettingsStore.getValue(name, ev.getRoomId());
// Hide redacted events // Hide redacted events
if (ev.isRedacted() && !isEnabled('showRedactions')) return true; if (ev.isRedacted() && !isEnabled('showRedactions')) return true;

View file

@ -54,8 +54,6 @@ const INITIAL_STATE = {
// Any error that has occurred during loading // Any error that has occurred during loading
roomLoadError: null, roomLoadError: null,
forwardingEvent: null,
quotingEvent: null, quotingEvent: null,
replyingToEvent: null, replyingToEvent: null,
@ -150,11 +148,6 @@ class RoomViewStore extends Store<ActionPayload> {
case 'on_logged_out': case 'on_logged_out':
this.reset(); this.reset();
break; break;
case 'forward_event':
this.setState({
forwardingEvent: payload.event,
});
break;
case 'reply_to_event': case 'reply_to_event':
// If currently viewed room does not match the room in which we wish to reply then change rooms // If currently viewed room does not match the room in which we wish to reply then change rooms
// this can happen when performing a search across all rooms // this can happen when performing a search across all rooms
@ -174,6 +167,7 @@ class RoomViewStore extends Store<ActionPayload> {
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, { Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
roomId: payload.room_id || this.state.roomId, roomId: payload.room_id || this.state.roomId,
initialTabId: payload.initial_tab_id,
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
break; break;
} }
@ -187,7 +181,6 @@ class RoomViewStore extends Store<ActionPayload> {
roomAlias: payload.room_alias, roomAlias: payload.room_alias,
initialEventId: payload.event_id, initialEventId: payload.event_id,
isInitialEventHighlighted: payload.highlighted, isInitialEventHighlighted: payload.highlighted,
forwardingEvent: null,
roomLoading: false, roomLoading: false,
roomLoadError: null, roomLoadError: null,
// should peek by default // should peek by default
@ -207,14 +200,6 @@ class RoomViewStore extends Store<ActionPayload> {
newState.replyingToEvent = payload.replyingToEvent; newState.replyingToEvent = payload.replyingToEvent;
} }
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'send_event',
room_id: newState.roomId,
event: this.state.forwardingEvent,
});
}
this.setState(newState); this.setState(newState);
if (payload.auto_join) { if (payload.auto_join) {
@ -428,11 +413,6 @@ class RoomViewStore extends Store<ActionPayload> {
return this.state.joinError; return this.state.joinError;
} }
// The mxEvent if one is about to be forwarded
public getForwardingEvent() {
return this.state.forwardingEvent;
}
// The mxEvent if one is currently being replied to/quoted // The mxEvent if one is currently being replied to/quoted
public getQuotingEvent() { public getQuotingEvent() {
return this.state.replyingToEvent; return this.state.replyingToEvent;

View file

@ -15,17 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// polyfill textencoder if necessary
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
let TextEncoder = window.TextEncoder;
if (!TextEncoder) {
TextEncoder = TextEncodingUtf8.TextEncoder;
}
let TextDecoder = window.TextDecoder;
if (!TextDecoder) {
TextDecoder = TextEncodingUtf8.TextDecoder;
}
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import SdkConfig from '../SdkConfig'; import SdkConfig from '../SdkConfig';

View file

@ -19,11 +19,11 @@ limitations under the License.
* TODO: Convert this to a real TypeScript interface * TODO: Convert this to a real TypeScript interface
*/ */
export default class PermalinkConstructor { export default class PermalinkConstructor {
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string {
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
@ -73,12 +73,12 @@ export class PermalinkParts {
return new PermalinkParts(null, null, null, groupId, null); return new PermalinkParts(null, null, null, groupId, null);
} }
static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts { static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []); return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
} }
static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts { static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
return new PermalinkParts(roomId, eventId, null, null, viaServers || []); return new PermalinkParts(roomId, eventId, null, null, viaServers);
} }
get primaryEntityId(): string { get primaryEntityId(): string {

View file

@ -149,7 +149,7 @@ export class RoomPermalinkCreator {
// Prefer to use canonical alias for permalink if possible // Prefer to use canonical alias for permalink if possible
const alias = this.room.getCanonicalAlias(); const alias = this.room.getCanonicalAlias();
if (alias) { if (alias) {
return getPermalinkConstructor().forRoom(alias, this._serverCandidates); return getPermalinkConstructor().forRoom(alias);
} }
} }
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
@ -302,7 +302,7 @@ export function makeRoomPermalink(roomId: string): string {
} }
const permalinkCreator = new RoomPermalinkCreator(room); const permalinkCreator = new RoomPermalinkCreator(room);
permalinkCreator.load(); permalinkCreator.load();
return permalinkCreator.forRoom(); return permalinkCreator.forShareableRoom();
} }
export function makeGroupPermalink(groupId: string): string { export function makeGroupPermalink(groupId: string): string {

View file

@ -16,7 +16,7 @@ limitations under the License.
import '../skinned-sdk'; // Must be first for skinning to work import '../skinned-sdk'; // Must be first for skinning to work
import React from "react"; import React from "react";
import Adapter from "enzyme-adapter-react-16"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { configure, mount } from "enzyme"; import { configure, mount } from "enzyme";
import { import {

View file

@ -32,7 +32,7 @@ import Matrix from 'matrix-js-sdk';
const test_utils = require('../../test-utils'); const test_utils = require('../../test-utils');
const mockclock = require('../../mock-clock'); const mockclock = require('../../mock-clock');
import Adapter from "enzyme-adapter-react-16"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { configure, mount } from "enzyme"; import { configure, mount } from "enzyme";
import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
@ -51,8 +51,20 @@ class WrappedMessagePanel extends React.Component {
}; };
render() { render() {
const roomContext = {
room,
roomId: room.roomId,
canReact: true,
canReply: true,
showReadReceipts: true,
showRedactions: false,
showJoinLeaves: false,
showAvatarChanges: false,
showDisplaynameChanges: true,
};
return <MatrixClientContext.Provider value={client}> return <MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={{ canReact: true, canReply: true, room, roomId: room.roomId }}> <RoomContext.Provider value={roomContext}>
<MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} /> <MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} />
</RoomContext.Provider> </RoomContext.Provider>
</MatrixClientContext.Provider>; </MatrixClientContext.Provider>;

View file

@ -0,0 +1,163 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../skinned-sdk";
import React from "react";
import {configure, mount} from "enzyme";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import {act} from "react-dom/test-utils";
import * as TestUtils from "../../../test-utils";
import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import {RoomPermalinkCreator} from "../../../../src/utils/permalinks/Permalinks";
import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
configure({ adapter: new Adapter() });
describe("ForwardDialog", () => {
const sourceRoom = "!111111111111111111:example.org";
const defaultMessage = TestUtils.mkMessage({
room: sourceRoom,
user: "@alice:example.org",
msg: "Hello world!",
event: true,
});
const defaultRooms = ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name));
const mountForwardDialog = async (message = defaultMessage, rooms = defaultRooms) => {
const client = MatrixClientPeg.get();
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
let wrapper;
await act(async () => {
wrapper = mount(
<ForwardDialog
matrixClient={client}
event={message}
permalinkCreator={new RoomPermalinkCreator(undefined, sourceRoom)}
onFinished={jest.fn()}
/>,
);
// Wait one tick for our profile data to load so the state update happens within act
await new Promise(resolve => setImmediate(resolve));
});
return wrapper;
};
beforeEach(() => {
TestUtils.stubClient();
DMRoomMap.makeShared();
MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue("@bob:example.org");
});
it("shows a preview with us as the sender", async () => {
const wrapper = await mountForwardDialog();
const previewBody = wrapper.find(".mx_EventTile_body");
expect(previewBody.text()).toBe("Hello world!");
// We would just test SenderProfile for the user ID, but it's stubbed
const previewAvatar = wrapper.find(".mx_EventTile_avatar .mx_BaseAvatar_image");
expect(previewAvatar.prop("title")).toBe("@bob:example.org");
});
it("filters the rooms", async () => {
const wrapper = await mountForwardDialog();
expect(wrapper.find("Entry")).toHaveLength(3);
const searchInput = wrapper.find("SearchBox input");
searchInput.instance().value = "a";
searchInput.simulate("change");
expect(wrapper.find("Entry")).toHaveLength(2);
});
it("tracks message sending progress across multiple rooms", async () => {
const wrapper = await mountForwardDialog();
// Make sendEvent require manual resolution so we can see the sending state
let finishSend;
let cancelSend;
MatrixClientPeg.get().sendEvent = jest.fn(() => new Promise((resolve, reject) => {
finishSend = resolve;
cancelSend = reject;
}));
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
expect(firstButton.render().is(".mx_ForwardList_canSend")).toBe(true);
act(() => { firstButton.simulate("click"); });
expect(firstButton.render().is(".mx_ForwardList_sending")).toBe(true);
await act(async () => {
cancelSend();
// Wait one tick for the button to realize the send failed
await new Promise(resolve => setImmediate(resolve));
});
expect(firstButton.render().is(".mx_ForwardList_sendFailed")).toBe(true);
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1);
expect(secondButton.render().is(".mx_ForwardList_canSend")).toBe(true);
act(() => { secondButton.simulate("click"); });
expect(secondButton.render().is(".mx_ForwardList_sending")).toBe(true);
await act(async () => {
finishSend();
// Wait one tick for the button to realize the send succeeded
await new Promise(resolve => setImmediate(resolve));
});
expect(secondButton.render().is(".mx_ForwardList_sent")).toBe(true);
});
it("can render replies", async () => {
const replyMessage = TestUtils.mkEvent({
type: "m.room.message",
room: "!111111111111111111:example.org",
user: "@alice:example.org",
content: {
"msgtype": "m.text",
"body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$2222222222222222222222222222222222222222222",
},
},
},
event: true,
});
const wrapper = await mountForwardDialog(replyMessage);
expect(wrapper.find("ReplyThread")).toBeTruthy();
});
it("disables buttons for rooms without send permissions", async () => {
const readOnlyRoom = TestUtils.mkStubRoom("a", "a");
readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
const rooms = [readOnlyRoom, TestUtils.mkStubRoom("b", "b")];
const wrapper = await mountForwardDialog(undefined, rooms);
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
expect(firstButton.prop("disabled")).toBe(true);
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last();
expect(secondButton.prop("disabled")).toBe(false);
});
});

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import Adapter from "enzyme-adapter-react-16"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { configure, mount } from "enzyme"; import { configure, mount } from "enzyme";
import sdk from "../../../skinned-sdk"; import sdk from "../../../skinned-sdk";

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import '../../../skinned-sdk'; // Must be first for skinning to work import '../../../skinned-sdk'; // Must be first for skinning to work
import Adapter from "enzyme-adapter-react-16"; import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { configure, mount } from "enzyme"; import { configure, mount } from "enzyme";
import React from "react"; import React from "react";
import {act} from "react-dom/test-utils"; import {act} from "react-dom/test-utils";

View file

@ -122,7 +122,7 @@ function getAllEventTiles(session) {
} }
async function getMessageFromEventTile(eventTile) { async function getMessageFromEventTile(eventTile) {
const senderElement = await eventTile.$(".mx_SenderProfile_name"); const senderElement = await eventTile.$(".mx_SenderProfile_displayName");
const className = await (await eventTile.getProperty("className")).jsonValue(); const className = await (await eventTile.getProperty("className")).jsonValue();
const classNames = className.split(" "); const classNames = className.split(" ");
const bodyElement = await eventTile.$(".mx_EventTile_body"); const bodyElement = await eventTile.$(".mx_EventTile_body");

View file

@ -79,7 +79,7 @@ async function runTests() {
await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
} }
const performanceEntries = {}; let performanceEntries;
await Promise.all(sessions.map(async (session) => { await Promise.all(sessions.map(async (session) => {
// Collecting all performance monitoring data before closing the session // Collecting all performance monitoring data before closing the session
@ -95,7 +95,11 @@ async function runTests() {
}, true); }, true);
return measurements; return measurements;
}); });
performanceEntries[session.username] = JSON.parse(measurements);
/**
* TODO: temporary only use one user session data
*/
performanceEntries = JSON.parse(measurements);
return session.close(); return session.close();
})); }));
fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries)); fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries));

View file

@ -760,9 +760,9 @@ wrappy@1:
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^6.1.0: ws@^6.1.0:
version "6.2.1" version "6.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
dependencies: dependencies:
async-limiter "~1.0.0" async-limiter "~1.0.0"

View file

@ -1,6 +1,12 @@
import * as languageHandler from "../src/languageHandler"; import * as languageHandler from "../src/languageHandler";
import { TextEncoder, TextDecoder } from 'util';
languageHandler.setLanguage('en'); languageHandler.setLanguage('en');
languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]); languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]);
require('jest-fetch-mock').enableMocks(); require('jest-fetch-mock').enableMocks();
// polyfilling TextEncoder as it is not available on JSDOM
// view https://github.com/facebook/jest/issues/9983
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;

View file

@ -219,7 +219,7 @@ export function mkMessage(opts) {
return mkEvent(opts); return mkEvent(opts);
} }
export function mkStubRoom(roomId = null) { export function mkStubRoom(roomId = null, name) {
const stubTimeline = { getEvents: () => [] }; const stubTimeline = { getEvents: () => [] };
return { return {
roomId, roomId,
@ -238,6 +238,7 @@ export function mkStubRoom(roomId = null) {
getPendingEvents: () => [], getPendingEvents: () => [],
getLiveTimeline: () => stubTimeline, getLiveTimeline: () => stubTimeline,
getUnfilteredTimelineSet: () => null, getUnfilteredTimelineSet: () => null,
findEventById: () => null,
getAccountData: () => null, getAccountData: () => null,
hasMembershipState: () => null, hasMembershipState: () => null,
getVersion: () => '1', getVersion: () => '1',
@ -255,13 +256,17 @@ export function mkStubRoom(roomId = null) {
tags: {}, tags: {},
setBlacklistUnverifiedDevices: jest.fn(), setBlacklistUnverifiedDevices: jest.fn(),
on: jest.fn(), on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(), removeListener: jest.fn(),
getDMInviter: jest.fn(), getDMInviter: jest.fn(),
name,
getAvatarUrl: () => 'mxc://avatar.url/room.png', getAvatarUrl: () => 'mxc://avatar.url/room.png',
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
isSpaceRoom: jest.fn(() => false), isSpaceRoom: jest.fn(() => false),
getUnreadNotificationCount: jest.fn(() => 0), getUnreadNotificationCount: jest.fn(() => 0),
getEventReadUpTo: jest.fn(() => null), getEventReadUpTo: jest.fn(() => null),
getCanonicalAlias: jest.fn(),
getAltAliases: jest.fn().mockReturnValue([]),
timeline: [], timeline: [],
}; };
} }

View file

@ -34,7 +34,7 @@ function mockRoom(roomId, members, serverACL) {
return { return {
roomId, roomId,
getCanonicalAlias: () => roomId, getCanonicalAlias: () => null,
getJoinedMembers: () => members, getJoinedMembers: () => members,
getMember: (userId) => members.find(m => m.userId === userId), getMember: (userId) => members.find(m => m.userId === userId),
currentState: { currentState: {

324
yarn.lock
View file

@ -1752,6 +1752,31 @@
"@typescript-eslint/types" "4.14.0" "@typescript-eslint/types" "4.14.0"
eslint-visitor-keys "^2.0.0" eslint-visitor-keys "^2.0.0"
"@wojtekmaj/enzyme-adapter-react-17@^0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.1.tgz#28caa37118c183e5f13c4dfb68cc32cde828ecbc"
integrity sha512-xgPfzLVpN0epIHeZofahwr5qwpukEDNAbrufgeDWN6vZPtfblGCC+OZG5TlfK+A6ePVy8sBkD8S2X4tO17JKjg==
dependencies:
"@wojtekmaj/enzyme-adapter-utils" "^0.1.0"
enzyme-shallow-equal "^1.0.0"
has "^1.0.0"
object.assign "^4.1.0"
object.values "^1.1.0"
prop-types "^15.7.0"
react-is "^17.0.0"
react-test-renderer "^17.0.0"
"@wojtekmaj/enzyme-adapter-utils@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.0.tgz#3a2a3db756111d53357e2f119a1612a969ab8c38"
integrity sha512-EYK/Vy0Y1ap0jH2UNQjOKtR/7HWkbEq8N+cwC5+yDf+Mwp5uu7j4Qg70RmWuzsA35DGGwgkop6m4pQsGwNOF2A==
dependencies:
function.prototype.name "^1.1.0"
has "^1.0.0"
object.assign "^4.1.0"
object.fromentries "^2.0.0"
prop-types "^15.7.0"
abab@^2.0.3: abab@^2.0.3:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
@ -1780,21 +1805,6 @@ acorn@^7.1.1, acorn@^7.4.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
airbnb-prop-types@^2.16.0:
version "2.16.0"
resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==
dependencies:
array.prototype.find "^2.1.1"
function.prototype.name "^1.1.2"
is-regex "^1.1.0"
object-is "^1.1.2"
object.assign "^4.1.0"
object.entries "^1.1.2"
prop-types "^15.7.2"
prop-types-exact "^1.2.0"
react-is "^16.13.1"
ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4:
version "6.12.6" version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -1920,14 +1930,6 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
array.prototype.find@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c"
integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.4"
array.prototype.flat@^1.2.3: array.prototype.flat@^1.2.3:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
@ -2179,11 +2181,6 @@ bluebird@^3.5.0:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
blueimp-canvas-to-blob@^3.28.0:
version "3.28.0"
resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.28.0.tgz#c8ab4dc6bb08774a7f273798cdf94b0776adf6c8"
integrity sha512-5q+YHzgGsuHQ01iouGgJaPJXod2AzTxJXmVv90PpGrRxU7G7IqgPqWXz+PBmt3520jKKi6irWbNV87DicEa7wg==
boolbase@^1.0.0: boolbase@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@ -2721,9 +2718,9 @@ css-select@^4.1.2:
nth-check "^2.0.0" nth-check "^2.0.0"
css-what@^5.0.0: css-what@^5.0.0:
version "5.0.0" version "5.0.1"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.0.tgz#f0bf4f8bac07582722346ab243f6a35b512cfc47" resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
integrity sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA== integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
cssesc@^3.0.0: cssesc@^3.0.0:
version "3.0.0" version "3.0.0"
@ -3075,35 +3072,7 @@ entities@~2.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
enzyme-adapter-react-16@^1.15.6: enzyme-shallow-equal@^1.0.0, enzyme-shallow-equal@^1.0.1:
version "1.15.6"
resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz#fd677a658d62661ac5afd7f7f541f141f8085901"
integrity sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g==
dependencies:
enzyme-adapter-utils "^1.14.0"
enzyme-shallow-equal "^1.0.4"
has "^1.0.3"
object.assign "^4.1.2"
object.values "^1.1.2"
prop-types "^15.7.2"
react-is "^16.13.1"
react-test-renderer "^16.0.0-0"
semver "^5.7.0"
enzyme-adapter-utils@^1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0"
integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg==
dependencies:
airbnb-prop-types "^2.16.0"
function.prototype.name "^1.1.3"
has "^1.0.3"
object.assign "^4.1.2"
object.fromentries "^2.0.3"
prop-types "^15.7.2"
semver "^5.7.1"
enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e"
integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==
@ -3146,7 +3115,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies: dependencies:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
es-abstract@^1.17.0-next.1, es-abstract@^1.17.4: es-abstract@^1.17.0-next.1:
version "1.17.7" version "1.17.7"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
@ -3183,6 +3152,28 @@ es-abstract@^1.18.0-next.1:
string.prototype.trimend "^1.0.3" string.prototype.trimend "^1.0.3"
string.prototype.trimstart "^1.0.3" string.prototype.trimstart "^1.0.3"
es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
version "1.18.3"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0"
integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==
dependencies:
call-bind "^1.0.2"
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
get-intrinsic "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.2"
is-callable "^1.2.3"
is-negative-zero "^2.0.1"
is-regex "^1.1.3"
is-string "^1.0.6"
object-inspect "^1.10.3"
object-keys "^1.1.1"
object.assign "^4.1.2"
string.prototype.trimend "^1.0.4"
string.prototype.trimstart "^1.0.4"
unbox-primitive "^1.0.1"
es-get-iterator@^1.0.1: es-get-iterator@^1.0.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.1.tgz#b93ddd867af16d5118e00881396533c1c6647ad9" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.1.tgz#b93ddd867af16d5118e00881396533c1c6647ad9"
@ -3965,7 +3956,17 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function.prototype.name@^1.1.2, function.prototype.name@^1.1.3: function.prototype.name@^1.1.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.4.tgz#e4ea839b9d3672ae99d0efd9f38d9191c5eaac83"
integrity sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
es-abstract "^1.18.0-next.2"
functions-have-names "^1.2.2"
function.prototype.name@^1.1.2:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe"
integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag== integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag==
@ -3980,7 +3981,7 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
functions-have-names@^1.2.0, functions-have-names@^1.2.1: functions-have-names@^1.2.0, functions-have-names@^1.2.1, functions-have-names@^1.2.2:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
@ -4004,6 +4005,15 @@ get-intrinsic@^1.0.1, get-intrinsic@^1.0.2:
has "^1.0.3" has "^1.0.3"
has-symbols "^1.0.1" has-symbols "^1.0.1"
get-intrinsic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
get-package-type@^0.1.0: get-package-type@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@ -4157,6 +4167,11 @@ hard-rejection@^2.1.0:
resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
has-flag@^3.0.0: has-flag@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@ -4172,6 +4187,11 @@ has-symbols@^1.0.1:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
has-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has-value@^0.3.1: has-value@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@ -4203,7 +4223,7 @@ has-values@^1.0.0:
is-number "^3.0.0" is-number "^3.0.0"
kind-of "^4.0.0" kind-of "^4.0.0"
has@^1.0.1, has@^1.0.3: has@^1.0.0, has@^1.0.1, has@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
@ -4534,6 +4554,11 @@ is-callable@^1.0.4, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
is-callable@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
is-ci@^2.0.0: is-ci@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
@ -4736,13 +4761,21 @@ is-potential-custom-element-name@^1.0.0:
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1: is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
dependencies: dependencies:
has-symbols "^1.0.1" has-symbols "^1.0.1"
is-regex@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
dependencies:
call-bind "^1.0.2"
has-symbols "^1.0.2"
is-regexp@^2.0.0: is-regexp@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
@ -4768,6 +4801,11 @@ is-string@^1.0.4, is-string@^1.0.5:
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-string@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
is-subset@^0.1.1: is-subset@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
@ -5695,10 +5733,10 @@ matrix-mock-request@^1.2.3:
bluebird "^3.5.0" bluebird "^3.5.0"
expect "^1.20.2" expect "^1.20.2"
matrix-react-test-utils@^0.2.2: matrix-react-test-utils@^0.2.3:
version "0.2.2" version "0.2.3"
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.3.tgz#27653f9d6bbfddd1856e51860fad1503b039d617"
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== integrity sha512-NKZDlMEQzDZDQhBYyKBUtqidRvpkww3n9/GmGICkxtU2D6NetyBIfvm1Lf9o7167KSkPHJUVvDS9dzaS55jUnA==
"matrix-web-i18n@github:matrix-org/matrix-web-i18n": "matrix-web-i18n@github:matrix-org/matrix-web-i18n":
version "1.1.2" version "1.1.2"
@ -6081,6 +6119,11 @@ object-inspect@^1.1.0, object-inspect@^1.7.0, object-inspect@^1.8.0, object-insp
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
object-inspect@^1.10.3:
version "1.10.3"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
object-is@^1.0.2, object-is@^1.1.2: object-is@^1.0.2, object-is@^1.1.2:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068"
@ -6121,7 +6164,17 @@ object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2:
es-abstract "^1.18.0-next.1" es-abstract "^1.18.0-next.1"
has "^1.0.3" has "^1.0.3"
object.fromentries@^2.0.2, object.fromentries@^2.0.3: object.fromentries@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8"
integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
es-abstract "^1.18.0-next.2"
has "^1.0.3"
object.fromentries@^2.0.2:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072"
integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw== integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==
@ -6138,7 +6191,16 @@ object.pick@^1.3.0:
dependencies: dependencies:
isobject "^3.0.1" isobject "^3.0.1"
object.values@^1.1.1, object.values@^1.1.2: object.values@^1.1.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
es-abstract "^1.18.2"
object.values@^1.1.1:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731"
integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag== integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==
@ -6521,9 +6583,9 @@ postcss-value-parser@^4.1.0:
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.6: postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.6:
version "7.0.35" version "7.0.36"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb"
integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==
dependencies: dependencies:
chalk "^2.4.2" chalk "^2.4.2"
source-map "^0.6.1" source-map "^0.6.1"
@ -6588,16 +6650,7 @@ prompts@^2.0.1:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types-exact@^1.2.0: prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
dependencies:
has "^1.0.3"
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -6729,15 +6782,14 @@ react-clientside-effect@^1.2.2:
dependencies: dependencies:
"@babel/runtime" "^7.0.0" "@babel/runtime" "^7.0.0"
react-dom@^16.14.0: react-dom@^17.0.2:
version "16.14.0" version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.2" scheduler "^0.20.2"
scheduler "^0.19.1"
react-focus-lock@^2.5.0: react-focus-lock@^2.5.0:
version "2.5.0" version "2.5.0"
@ -6751,7 +6803,12 @@ react-focus-lock@^2.5.0:
use-callback-ref "^1.2.1" use-callback-ref "^1.2.1"
use-sidecar "^1.0.1" use-sidecar "^1.0.1"
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: "react-is@^16.12.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -6788,15 +6845,23 @@ react-redux@^5.0.6:
react-is "^16.6.0" react-is "^16.6.0"
react-lifecycles-compat "^3.0.0" react-lifecycles-compat "^3.0.0"
react-test-renderer@^16.0.0-0, react-test-renderer@^16.14.0: react-shallow-renderer@^16.13.1:
version "16.14.0" version "16.14.1"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124"
integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==
dependencies: dependencies:
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.2" react-is "^16.12.0 || ^17.0.0"
react-is "^16.8.6"
scheduler "^0.19.1" react-test-renderer@^17.0.0, react-test-renderer@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c"
integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==
dependencies:
object-assign "^4.1.1"
react-is "^17.0.2"
react-shallow-renderer "^16.13.1"
scheduler "^0.20.2"
react-transition-group@^4.4.1: react-transition-group@^4.4.1:
version "4.4.1" version "4.4.1"
@ -6808,14 +6873,13 @@ react-transition-group@^4.4.1:
loose-envify "^1.4.0" loose-envify "^1.4.0"
prop-types "^15.6.2" prop-types "^15.6.2"
react@^16.14.0: react@^17.0.2:
version "16.14.0" version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.2"
read-pkg-up@^2.0.0: read-pkg-up@^2.0.0:
version "2.0.0" version "2.0.0"
@ -6923,11 +6987,6 @@ redux@^3.7.2:
loose-envify "^1.1.0" loose-envify "^1.1.0"
symbol-observable "^1.0.3" symbol-observable "^1.0.3"
reflect.ownkeys@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
regenerate-unicode-properties@^8.2.0: regenerate-unicode-properties@^8.2.0:
version "8.2.0" version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@ -7266,15 +7325,15 @@ saxes@^5.0.0:
dependencies: dependencies:
xmlchars "^2.2.0" xmlchars "^2.2.0"
scheduler@^0.19.1: scheduler@^0.20.2:
version "0.19.1" version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -7622,6 +7681,14 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3:
call-bind "^1.0.0" call-bind "^1.0.0"
define-properties "^1.1.3" define-properties "^1.1.3"
string.prototype.trimend@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3: string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
@ -7630,6 +7697,14 @@ string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3:
call-bind "^1.0.0" call-bind "^1.0.0"
define-properties "^1.1.3" define-properties "^1.1.3"
string.prototype.trimstart@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
string_decoder@^1.1.1: string_decoder@^1.1.1:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@ -7865,11 +7940,6 @@ test-exclude@^6.0.0:
glob "^7.1.4" glob "^7.1.4"
minimatch "^3.0.4" minimatch "^3.0.4"
text-encoding-utf-8@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
text-table@^0.2.0: text-table@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -7969,9 +8039,9 @@ tree-kill@^1.2.2:
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
trim-newlines@^3.0.0: trim-newlines@^3.0.0:
version "3.0.0" version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
trough@^1.0.0: trough@^1.0.0:
version "1.0.5" version "1.0.5"
@ -8078,6 +8148,16 @@ ua-parser-js@^0.7.18:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
unbox-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
dependencies:
function-bind "^1.1.1"
has-bigints "^1.0.1"
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
unhomoglyph@^1.0.6: unhomoglyph@^1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3" resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3"
@ -8352,7 +8432,7 @@ whatwg-url@^8.0.0:
tr46 "^2.0.2" tr46 "^2.0.2"
webidl-conversions "^6.1.0" webidl-conversions "^6.1.0"
which-boxed-primitive@^1.0.1: which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==