mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 03:36:07 +03:00
Merge remote-tracking branch 'origin/develop' into jaywink/elementPro
This commit is contained in:
commit
5aa24b97cd
145 changed files with 7274 additions and 2269 deletions
|
@ -1,4 +1,4 @@
|
|||
src/component-index.js
|
||||
test/end-to-end-tests/node_modules/
|
||||
test/end-to-end-tests/riot/
|
||||
test/end-to-end-tests/element/
|
||||
test/end-to-end-tests/synapse/
|
||||
|
|
|
@ -12,5 +12,5 @@ test/components/views/dialogs/InteractiveAuthDialog-test.js
|
|||
test/mock-clock.js
|
||||
src/component-index.js
|
||||
test/end-to-end-tests/node_modules/
|
||||
test/end-to-end-tests/riot/
|
||||
test/end-to-end-tests/element/
|
||||
test/end-to-end-tests/synapse/
|
||||
|
|
83
CHANGELOG.md
83
CHANGELOG.md
|
@ -1,3 +1,86 @@
|
|||
Changes in [3.10.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0) (2020-12-07)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.10.0-rc.1...v3.10.0)
|
||||
|
||||
* Upgrade to JS SDK 9.3.0
|
||||
|
||||
Changes in [3.10.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0-rc.1) (2020-12-02)
|
||||
===============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0...v3.10.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 9.3.0-rc.1
|
||||
* Translations update from Weblate
|
||||
[\#5461](https://github.com/matrix-org/matrix-react-sdk/pull/5461)
|
||||
* Fix VoIP call plinth on dark theme
|
||||
[\#5460](https://github.com/matrix-org/matrix-react-sdk/pull/5460)
|
||||
* Add sanity checking around widget pinning
|
||||
[\#5459](https://github.com/matrix-org/matrix-react-sdk/pull/5459)
|
||||
* Update i18n for Appearance User Settings
|
||||
[\#5457](https://github.com/matrix-org/matrix-react-sdk/pull/5457)
|
||||
* Only show 'answered elsewhere' if we tried to answer too
|
||||
[\#5455](https://github.com/matrix-org/matrix-react-sdk/pull/5455)
|
||||
* Fixed Avatar for 3PID invites
|
||||
[\#5442](https://github.com/matrix-org/matrix-react-sdk/pull/5442)
|
||||
* Slightly better error if we can't capture user media
|
||||
[\#5449](https://github.com/matrix-org/matrix-react-sdk/pull/5449)
|
||||
* Make it possible in-code to hide rooms from the room list
|
||||
[\#5445](https://github.com/matrix-org/matrix-react-sdk/pull/5445)
|
||||
* Fix the stickerpicker
|
||||
[\#5447](https://github.com/matrix-org/matrix-react-sdk/pull/5447)
|
||||
* Add live password validation to change password dialog
|
||||
[\#5436](https://github.com/matrix-org/matrix-react-sdk/pull/5436)
|
||||
* LaTeX rendering in element-web using KaTeX
|
||||
[\#5244](https://github.com/matrix-org/matrix-react-sdk/pull/5244)
|
||||
* Add lifecycle customisation point after logout
|
||||
[\#5448](https://github.com/matrix-org/matrix-react-sdk/pull/5448)
|
||||
* Simplify UserMenu for Guests as they can't use most of the options
|
||||
[\#5421](https://github.com/matrix-org/matrix-react-sdk/pull/5421)
|
||||
* Fix known issues with modal widgets
|
||||
[\#5444](https://github.com/matrix-org/matrix-react-sdk/pull/5444)
|
||||
* Fix existing widgets not having approved capabilities for their function
|
||||
[\#5443](https://github.com/matrix-org/matrix-react-sdk/pull/5443)
|
||||
* Use the WidgetDriver to run OIDC requests
|
||||
[\#5440](https://github.com/matrix-org/matrix-react-sdk/pull/5440)
|
||||
* Add a customisation point for widget permissions and fix amnesia issues
|
||||
[\#5439](https://github.com/matrix-org/matrix-react-sdk/pull/5439)
|
||||
* Fix Widget event notification text including spurious space
|
||||
[\#5441](https://github.com/matrix-org/matrix-react-sdk/pull/5441)
|
||||
* Move call listener out of MatrixChat
|
||||
[\#5438](https://github.com/matrix-org/matrix-react-sdk/pull/5438)
|
||||
* New Look in-Call View
|
||||
[\#5432](https://github.com/matrix-org/matrix-react-sdk/pull/5432)
|
||||
* Support arbitrary widgets sticking to the screen + sending stickers
|
||||
[\#5435](https://github.com/matrix-org/matrix-react-sdk/pull/5435)
|
||||
* Auth typescripting and validation tweaks
|
||||
[\#5433](https://github.com/matrix-org/matrix-react-sdk/pull/5433)
|
||||
* Add new widget API actions for changing rooms and sending/receiving events
|
||||
[\#5385](https://github.com/matrix-org/matrix-react-sdk/pull/5385)
|
||||
* Revert room header click behaviour to opening room settings
|
||||
[\#5434](https://github.com/matrix-org/matrix-react-sdk/pull/5434)
|
||||
* Add option to send/edit a message with Ctrl + Enter / Command + Enter
|
||||
[\#5160](https://github.com/matrix-org/matrix-react-sdk/pull/5160)
|
||||
* Add Analytics instrumentation to the Homepage
|
||||
[\#5409](https://github.com/matrix-org/matrix-react-sdk/pull/5409)
|
||||
* Fix encrypted video playback in Chrome-based browsers
|
||||
[\#5430](https://github.com/matrix-org/matrix-react-sdk/pull/5430)
|
||||
* Add border-radius for video
|
||||
[\#5333](https://github.com/matrix-org/matrix-react-sdk/pull/5333)
|
||||
* Push name to the end, near text, in IRC layout
|
||||
[\#5166](https://github.com/matrix-org/matrix-react-sdk/pull/5166)
|
||||
* Disable notifications for the room you have recently been active in
|
||||
[\#5325](https://github.com/matrix-org/matrix-react-sdk/pull/5325)
|
||||
* Search through the list of unfiltered rooms rather than the rooms in the
|
||||
state which are already filtered by the search text
|
||||
[\#5331](https://github.com/matrix-org/matrix-react-sdk/pull/5331)
|
||||
* Lighten blockquote colour in dark mode
|
||||
[\#5353](https://github.com/matrix-org/matrix-react-sdk/pull/5353)
|
||||
* Specify community description img must be mxc urls
|
||||
[\#5364](https://github.com/matrix-org/matrix-react-sdk/pull/5364)
|
||||
* Add keyboard shortcut to close the current conversation
|
||||
[\#5253](https://github.com/matrix-org/matrix-react-sdk/pull/5253)
|
||||
* Redirect user home from auth screens if they are already logged in
|
||||
[\#5423](https://github.com/matrix-org/matrix-react-sdk/pull/5423)
|
||||
|
||||
Changes in [3.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0) (2020-11-23)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0-rc.1...v3.9.0)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.9.0",
|
||||
"version": "3.10.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -50,7 +50,7 @@
|
|||
"lint:types": "tsc --noEmit --jsx react",
|
||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||
"test": "jest",
|
||||
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
|
||||
"test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.5",
|
||||
|
@ -58,6 +58,7 @@
|
|||
"blueimp-canvas-to-blob": "^3.27.0",
|
||||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"classnames": "^2.2.6",
|
||||
"commonmark": "^0.29.1",
|
||||
"counterpart": "^0.18.6",
|
||||
|
@ -77,7 +78,6 @@
|
|||
"html-entities": "^1.3.1",
|
||||
"is-ip": "^2.0.0",
|
||||
"katex": "^0.12.0",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.19",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
|
@ -159,6 +159,7 @@
|
|||
"lolex": "^5.1.2",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-react-test-utils": "^0.2.2",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"rimraf": "^2.7.1",
|
||||
"stylelint": "^9.10.1",
|
||||
|
|
|
@ -170,7 +170,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
border: 1px solid rgba($primary-fg-color, .1);
|
||||
// these things should probably not be defined globally
|
||||
margin: 9px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mx_textinput {
|
||||
|
|
|
@ -45,14 +45,13 @@
|
|||
@import "./views/auth/_InteractiveAuthEntryComponents.scss";
|
||||
@import "./views/auth/_LanguageSelector.scss";
|
||||
@import "./views/auth/_PassphraseField.scss";
|
||||
@import "./views/auth/_ServerConfig.scss";
|
||||
@import "./views/auth/_ServerTypeSelector.scss";
|
||||
@import "./views/auth/_Welcome.scss";
|
||||
@import "./views/avatars/_BaseAvatar.scss";
|
||||
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||
@import "./views/avatars/_PulsedAvatar.scss";
|
||||
@import "./views/avatars/_WidgetAvatar.scss";
|
||||
@import "./views/context_menus/_CallContextMenu.scss";
|
||||
@import "./views/context_menus/_IconizedContextMenu.scss";
|
||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||
|
@ -79,11 +78,13 @@
|
|||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||
@import "./views/dialogs/_ModalWidgetDialog.scss";
|
||||
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
||||
@import "./views/dialogs/_RegistrationEmailPromptDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
|
||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
|
||||
@import "./views/dialogs/_ServerOfflineDialog.scss";
|
||||
@import "./views/dialogs/_ServerPickerDialog.scss";
|
||||
@import "./views/dialogs/_SetEmailDialog.scss";
|
||||
@import "./views/dialogs/_SettingsDialog.scss";
|
||||
@import "./views/dialogs/_ShareDialog.scss";
|
||||
|
@ -125,6 +126,8 @@
|
|||
@import "./views/elements/_RichText.scss";
|
||||
@import "./views/elements/_RoleButton.scss";
|
||||
@import "./views/elements/_RoomAliasField.scss";
|
||||
@import "./views/elements/_SSOButtons.scss";
|
||||
@import "./views/elements/_ServerPicker.scss";
|
||||
@import "./views/elements/_Slider.scss";
|
||||
@import "./views/elements/_Spinner.scss";
|
||||
@import "./views/elements/_StyledCheckbox.scss";
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
.mx_Login_submit {
|
||||
@mixin mx_DialogButton;
|
||||
width: 100%;
|
||||
margin-top: 35px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
|
@ -33,12 +33,6 @@ limitations under the License.
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.mx_AuthBody a.mx_Login_sso_link:link,
|
||||
.mx_AuthBody a.mx_Login_sso_link:hover,
|
||||
.mx_AuthBody a.mx_Login_sso_link:visited {
|
||||
color: $button-primary-fg-color;
|
||||
}
|
||||
|
||||
.mx_Login_loader {
|
||||
display: inline;
|
||||
position: relative;
|
||||
|
@ -87,10 +81,13 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_Login_underlinedServerName {
|
||||
width: max-content;
|
||||
border-bottom: 1px dashed $accent-color;
|
||||
}
|
||||
|
||||
div.mx_AccessibleButton_kind_link.mx_Login_forgot {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
// style it as a link
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
|
|
|
@ -37,6 +37,10 @@ limitations under the License.
|
|||
color: $authpage-primary-color;
|
||||
}
|
||||
|
||||
h3.mx_AuthBody_centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a:link,
|
||||
a:hover,
|
||||
a:visited {
|
||||
|
@ -96,12 +100,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_AuthBody_editServerDetails {
|
||||
padding-left: 1em;
|
||||
font-size: $font-12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.mx_AuthBody_fieldRow {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
@ -146,6 +144,14 @@ limitations under the License.
|
|||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
> a {
|
||||
font-weight: $font-semi-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SSOButtons + .mx_AuthBody_changeFlow {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mx_AuthBody_spinner {
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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_ServerTypeSelector {
|
||||
display: flex;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_type {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_type:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_type:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_label {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: $authpage-primary-color;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_type .mx_AccessibleButton {
|
||||
padding: 10px;
|
||||
border: 1px solid $input-border-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_type.mx_ServerTypeSelector_type_selected .mx_AccessibleButton {
|
||||
border-color: $input-valid-border-color;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 18px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
color: $authpage-primary-color;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_logo > div {
|
||||
display: flex;
|
||||
width: 70%;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.mx_ServerTypeSelector_description {
|
||||
font-size: $font-10px;
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,21 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ServerConfig_help:link {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mx_ServerConfig_error {
|
||||
display: block;
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
.mx_ServerConfig_identityServer {
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition: transform 0.25s;
|
||||
|
||||
&.mx_ServerConfig_identityServer_shown {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
.mx_CallContextMenu_item {
|
||||
width: 205px;
|
||||
height: 40px;
|
||||
padding-left: 16px;
|
||||
line-height: 40px;
|
||||
vertical-align: center;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,18 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
.mx_RegistrationEmailPromptDialog {
|
||||
width: 417px;
|
||||
|
||||
function tsOfNewestEvent(room) {
|
||||
if (room.timeline.length) {
|
||||
return room.timeline[room.timeline.length - 1].getTs();
|
||||
} else {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
.mx_Dialog_content {
|
||||
margin-bottom: 24px;
|
||||
color: $tertiary-fg-color;
|
||||
}
|
||||
|
||||
.mx_Dialog_primary {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
export function mostRecentActivityFirst(roomList) {
|
||||
return roomList.sort(function(a, b) {
|
||||
return tsOfNewestEvent(b) - tsOfNewestEvent(a);
|
||||
});
|
||||
}
|
78
res/css/views/dialogs/_ServerPickerDialog.scss
Normal file
78
res/css/views/dialogs/_ServerPickerDialog.scss
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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_ServerPickerDialog {
|
||||
width: 468px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.mx_Dialog_content {
|
||||
margin-bottom: 0;
|
||||
|
||||
> p {
|
||||
color: $secondary-fg-color;
|
||||
font-size: $font-14px;
|
||||
margin: 16px 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin: 0 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
> h4 {
|
||||
font-size: $font-15px;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $secondary-fg-color;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
> a {
|
||||
color: $accent-color;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ServerPickerDialog_otherHomeserverRadio {
|
||||
input[type="radio"] + div {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ServerPickerDialog_otherHomeserver {
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: unset;
|
||||
|
||||
> input {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
> label {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_primary {
|
||||
width: calc(100% - 64px);
|
||||
margin: 0 8px;
|
||||
padding: 15px 18px;
|
||||
}
|
||||
}
|
49
res/css/views/elements/_SSOButtons.scss
Normal file
49
res/css/views/elements/_SSOButtons.scss
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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_SSOButtons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.mx_SSOButton {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-left: 32px;
|
||||
padding-right: 32px;
|
||||
|
||||
> img {
|
||||
object-fit: contain;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SSOButton_mini {
|
||||
box-sizing: border-box;
|
||||
width: 50px; // 48px + 1px border on all sides
|
||||
height: 50px; // 48px + 1px border on all sides
|
||||
|
||||
> img {
|
||||
left: 12px;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
& + .mx_SSOButton_mini {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
}
|
88
res/css/views/elements/_ServerPicker.scss
Normal file
88
res/css/views/elements/_ServerPicker.scss
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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_ServerPicker {
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 1px solid rgba(141, 151, 165, 0.2);
|
||||
display: grid;
|
||||
grid-template-columns: auto min-content;
|
||||
grid-template-rows: auto auto auto;
|
||||
font-size: $font-14px;
|
||||
line-height: $font-20px;
|
||||
|
||||
> h3 {
|
||||
font-weight: $font-semi-bold;
|
||||
margin: 0 0 20px;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.mx_ServerPicker_help {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: $icon-button-color;
|
||||
border-radius: 10px;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin-left: auto;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url('$(res)/img/element-icons/i.svg');
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ServerPicker_server {
|
||||
color: $primary-fg-color;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mx_ServerPicker_change {
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mx_ServerPicker_desc {
|
||||
margin-top: -12px;
|
||||
color: $tertiary-fg-color;
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 3;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ServerPicker_helpDialog {
|
||||
.mx_Dialog_content {
|
||||
width: 456px;
|
||||
}
|
||||
}
|
|
@ -46,6 +46,11 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_GroupMemberList_query,
|
||||
.mx_GroupRoomList_query {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mx_MemberList_chevron {
|
||||
position: absolute;
|
||||
right: 35px;
|
||||
|
@ -59,10 +64,8 @@ limitations under the License.
|
|||
flex: 1 1 0px;
|
||||
}
|
||||
|
||||
.mx_MemberList_query,
|
||||
.mx_GroupMemberList_query,
|
||||
.mx_GroupRoomList_query {
|
||||
flex: 1 1 0;
|
||||
.mx_MemberList_query {
|
||||
height: 16px;
|
||||
|
||||
// stricter rule to override the one in _common.scss
|
||||
&[type="text"] {
|
||||
|
@ -70,10 +73,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_MemberList_query {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.mx_MemberList_wrapper {
|
||||
padding: 10px;
|
||||
}
|
||||
|
@ -113,10 +112,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_MemberList_inviteCommunity span {
|
||||
background-image: url('$(res)/img/icon-invite-people.svg');
|
||||
.mx_MemberList_inviteCommunity span::before {
|
||||
mask-image: url('$(res)/img/icon-invite-people.svg');
|
||||
}
|
||||
|
||||
.mx_MemberList_addRoomToCommunity span {
|
||||
background-image: url('$(res)/img/icons-room-add.svg');
|
||||
.mx_MemberList_addRoomToCommunity span::before {
|
||||
mask-image: url('$(res)/img/icons-room-add.svg');
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
iframe {
|
||||
// Sticker picker depends on the fixed height previously used for all tiles
|
||||
height: 273px;
|
||||
height: 283px; // height of the popout minus the AppTile menu bar
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_CallView {
|
||||
border-radius: 10px;
|
||||
background-color: $input-lighter-bg-color;
|
||||
border-radius: 8px;
|
||||
background-color: $voipcall-plinth-color;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
|
||||
|
@ -26,6 +26,7 @@ limitations under the License.
|
|||
|
||||
.mx_CallView_large {
|
||||
padding-bottom: 10px;
|
||||
margin: 5px 5px 5px 18px;
|
||||
|
||||
.mx_CallView_voice {
|
||||
height: 360px;
|
||||
|
@ -38,20 +39,176 @@ limitations under the License.
|
|||
.mx_CallView_voice {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.mx_CallView_callControls {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.mx_CallView_callControls_button {
|
||||
&::before {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_holdText {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $inverted-bg-color;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_CallView_voice_avatarsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
div {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_hold {
|
||||
// This masks the avatar image so when it's blurred, the edge is still crisp
|
||||
.mx_CallView_voice_avatarContainer {
|
||||
border-radius: 2000px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
background-image: url('$(res)/img/voip/paused.svg');
|
||||
background-position: center;
|
||||
background-size: 40px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.mx_CallView_pip &::after {
|
||||
background-size: 30px;
|
||||
}
|
||||
}
|
||||
.mx_BaseAvatar {
|
||||
filter: blur(20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_secondaryAvatarContainer {
|
||||
border-radius: 2000px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
background-image: url('$(res)/img/voip/paused.svg');
|
||||
background-position: center;
|
||||
background-size: 40px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.mx_CallView_pip &::after {
|
||||
background-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_voice_holdText {
|
||||
height: 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 15px;
|
||||
color: $accent-fg-color;
|
||||
.mx_AccessibleButton_hasKind {
|
||||
padding: 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_video {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_CallView_video_hold {
|
||||
overflow: hidden;
|
||||
|
||||
// we keep these around in the DOM: it saved wiring them up again when the call
|
||||
// is resumed and keeps the container the right size
|
||||
.mx_VideoFeed {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_video_holdBackground {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: blur(20px);
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_video_holdContent {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: bold;
|
||||
color: $accent-fg-color;
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
content: '';
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-image: url('$(res)/img/voip/paused.svg');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
.mx_CallView_pip &::before {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.mx_AccessibleButton_hasKind {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_header {
|
||||
|
@ -60,17 +217,22 @@ limitations under the License.
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
|
||||
.mx_BaseAvatar {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_header_callType {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_CallView_header_secondaryCallInfo {
|
||||
&::before {
|
||||
content: '·';
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_header_controls {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
@ -105,16 +267,31 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_header_callInfo {
|
||||
margin-left: 12px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.mx_CallView_header_roomName {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
line-height: initial;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.mx_CallView_secondaryCall_roomName {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.mx_CallView_header_callTypeSmall {
|
||||
font-size: 12px;
|
||||
color: $secondary-fg-color;
|
||||
line-height: initial;
|
||||
height: 15px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.mx_CallView_header_phoneIcon {
|
||||
|
@ -173,6 +350,12 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
// Makes the alignment correct
|
||||
.mx_CallView_callControls_nothing {
|
||||
margin-right: auto;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.mx_CallView_callControls_button_micOn {
|
||||
&::before {
|
||||
background-image: url('$(res)/img/voip/mic-on.svg');
|
||||
|
@ -203,6 +386,18 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_callControls_button_more {
|
||||
margin-left: auto;
|
||||
&::before {
|
||||
background-image: url('$(res)/img/voip/more.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_callControls_button_more_hidden {
|
||||
margin-left: auto;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.mx_CallView_callControls_button_invisible {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
|
3
res/img/element-icons/i.svg
Normal file
3
res/img/element-icons/i.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 10C12.8284 10 13.5 9.32843 13.5 8.5C13.5 7.67157 12.8284 7 12 7C11.1716 7 10.5 7.67157 10.5 8.5C10.5 9.32843 11.1716 10 12 10ZM11 13C10.4477 13 10 12.5523 10 12C10 11.4477 10.4477 11 11 11H12C12.5523 11 13 11.4477 13 12V15.5H13.5C14.0523 15.5 14.5 15.9477 14.5 16.5C14.5 17.0523 14.0523 17.5 13.5 17.5H12C11.4477 17.5 11 17.0523 11 16.5L11 13Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 516 B |
17
res/img/voip/more.svg
Normal file
17
res/img/voip/more.svg
Normal file
|
@ -0,0 +1,17 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d)">
|
||||
<circle cx="24" cy="20" r="20" fill="white"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.667 20C18.667 21.1046 17.7716 22 16.667 22C15.5624 22 14.667 21.1046 14.667 20C14.667 18.8954 15.5624 18 16.667 18C17.7716 18 18.667 18.8954 18.667 20ZM26 20C26 21.1046 25.1046 22 24 22C22.8954 22 22 21.1046 22 20C22 18.8954 22.8954 18 24 18C25.1046 18 26 18.8954 26 20ZM31.333 22C32.4376 22 33.333 21.1046 33.333 20C33.333 18.8954 32.4376 18 31.333 18C30.2284 18 29.333 18.8954 29.333 20C29.333 21.1046 30.2284 22 31.333 22Z" fill="#737D8C"/>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="0" y="0" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
res/img/voip/paused.svg
Normal file
3
res/img/voip/paused.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM11 16H9V8H11V16ZM15 16H13V8H15V16Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 256 B |
|
@ -108,6 +108,9 @@ $eventtile-meta-color: $roomtopic-color;
|
|||
$header-divider-color: $header-panel-text-primary-color;
|
||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||
|
||||
// this probably shouldn't have it's own colour
|
||||
$voipcall-plinth-color: #21262c;
|
||||
|
||||
// ********************
|
||||
|
||||
$theme-button-bg-color: #e3e8f0;
|
||||
|
@ -214,7 +217,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
|
|||
/* align images in buttons (eg spinners) */
|
||||
vertical-align: middle;
|
||||
border: 0px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
font-family: $font-family;
|
||||
font-size: $font-14px;
|
||||
color: $button-fg-color;
|
||||
|
|
|
@ -105,6 +105,9 @@ $eventtile-meta-color: $roomtopic-color;
|
|||
$header-divider-color: $header-panel-text-primary-color;
|
||||
$composer-e2e-icon-color: $header-panel-text-primary-color;
|
||||
|
||||
// this probably shouldn't have it's own colour
|
||||
$voipcall-plinth-color: #f2f5f8;
|
||||
|
||||
// ********************
|
||||
|
||||
$theme-button-bg-color: #e3e8f0;
|
||||
|
@ -205,7 +208,7 @@ $composer-shadow-color: tranparent;
|
|||
/* align images in buttons (eg spinners) */
|
||||
vertical-align: middle;
|
||||
border: 0px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
font-family: $font-family;
|
||||
font-size: $font-14px;
|
||||
color: $button-fg-color;
|
||||
|
|
|
@ -172,6 +172,9 @@ $eventtile-meta-color: $roomtopic-color;
|
|||
$composer-e2e-icon-color: #91a1c0;
|
||||
$header-divider-color: #91a1c0;
|
||||
|
||||
// this probably shouldn't have it's own colour
|
||||
$voipcall-plinth-color: #f2f5f8;
|
||||
|
||||
// ********************
|
||||
|
||||
$theme-button-bg-color: #e3e8f0;
|
||||
|
@ -328,7 +331,7 @@ $composer-shadow-color: tranparent;
|
|||
/* align images in buttons (eg spinners) */
|
||||
vertical-align: middle;
|
||||
border: 0px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
font-family: $font-family;
|
||||
font-size: $font-14px;
|
||||
color: $button-fg-color;
|
||||
|
|
|
@ -166,6 +166,9 @@ $eventtile-meta-color: $roomtopic-color;
|
|||
$composer-e2e-icon-color: #91A1C0;
|
||||
$header-divider-color: #91A1C0;
|
||||
|
||||
// this probably shouldn't have it's own colour
|
||||
$voipcall-plinth-color: #f2f5f8;
|
||||
|
||||
// ********************
|
||||
|
||||
$theme-button-bg-color: #e3e8f0;
|
||||
|
@ -332,7 +335,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04);
|
|||
/* align images in buttons (eg spinners) */
|
||||
vertical-align: middle;
|
||||
border: 0px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
font-family: $font-family;
|
||||
font-size: $font-14px;
|
||||
color: $button-fg-color;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Update on docker hub with the following commands in the directory of this file:
|
||||
# docker build -t matrixdotorg/riotweb-ci-e2etests-env:latest .
|
||||
# docker build -t vectorim/element-web-ci-e2etests-env:latest .
|
||||
# docker log
|
||||
# docker push matrixdotorg/riotweb-ci-e2etests-env:latest
|
||||
# docker push vectorim/element-web-ci-e2etests-env:latest
|
||||
FROM node:10
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
#
|
||||
# script which is run by the CI build (after `yarn test`).
|
||||
#
|
||||
# clones riot-web develop and runs the tests against our version of react-sdk.
|
||||
# clones element-web develop and runs the tests against our version of react-sdk.
|
||||
|
||||
set -ev
|
||||
|
||||
scripts/ci/layered-riot-web.sh
|
||||
cd ../riot-web
|
||||
scripts/ci/layered.sh
|
||||
cd element-web
|
||||
yarn build:genfiles # so the tests can run. Faster version of `build`
|
||||
yarn test
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
# script which is run by the CI build (after `yarn test`).
|
||||
#
|
||||
# clones riot-web develop and runs the tests against our version of react-sdk.
|
||||
# clones element-web develop and runs the tests against our version of react-sdk.
|
||||
|
||||
set -ev
|
||||
|
||||
|
@ -14,20 +14,20 @@ handle_error() {
|
|||
trap 'handle_error' ERR
|
||||
|
||||
echo "--- Building Element"
|
||||
scripts/ci/layered-riot-web.sh
|
||||
cd ../riot-web
|
||||
riot_web_dir=`pwd`
|
||||
scripts/ci/layered.sh
|
||||
cd element-web
|
||||
element_web_dir=`pwd`
|
||||
CI_PACKAGE=true yarn build
|
||||
cd ../matrix-react-sdk
|
||||
cd ..
|
||||
# run end to end tests
|
||||
pushd test/end-to-end-tests
|
||||
ln -s $riot_web_dir riot/riot-web
|
||||
ln -s $element_web_dir element/element-web
|
||||
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
|
||||
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
|
||||
echo "--- Install synapse & other dependencies"
|
||||
./install.sh
|
||||
# install static webserver to server symlinked local copy of riot
|
||||
./riot/install-webserver.sh
|
||||
# install static webserver to server symlinked local copy of element
|
||||
./element/install-webserver.sh
|
||||
rm -r logs || true
|
||||
mkdir logs
|
||||
echo "+++ Running end-to-end tests"
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Creates an environment similar to one that riot-web would expect for
|
||||
# development. This means going one directory up (and assuming we're in
|
||||
# a directory like /workdir/matrix-react-sdk) and putting riot-web and
|
||||
# the js-sdk there.
|
||||
|
||||
cd ../ # Assume we're at something like /workdir/matrix-react-sdk
|
||||
|
||||
# Set up the js-sdk first
|
||||
matrix-react-sdk/scripts/fetchdep.sh matrix-org matrix-js-sdk
|
||||
pushd matrix-js-sdk
|
||||
yarn link
|
||||
yarn install
|
||||
popd
|
||||
|
||||
# Now set up the react-sdk
|
||||
pushd matrix-react-sdk
|
||||
yarn link matrix-js-sdk
|
||||
yarn link
|
||||
yarn install
|
||||
popd
|
||||
|
||||
# Finally, set up riot-web
|
||||
matrix-react-sdk/scripts/fetchdep.sh vector-im riot-web
|
||||
pushd riot-web
|
||||
yarn link matrix-js-sdk
|
||||
yarn link matrix-react-sdk
|
||||
yarn install
|
||||
yarn build:res
|
||||
popd
|
31
scripts/ci/layered.sh
Executable file
31
scripts/ci/layered.sh
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Creates a layered environment with the full repo for the app and SDKs cloned
|
||||
# and linked.
|
||||
|
||||
# Note that this style is different from the recommended developer setup: this
|
||||
# file nests js-sdk and element-web inside react-sdk, while the local
|
||||
# development setup places them all at the same level. We are nesting them here
|
||||
# because some CI systems do not allow moving to a directory above the checkout
|
||||
# for the primary repo (react-sdk in this case).
|
||||
|
||||
# Set up the js-sdk first
|
||||
scripts/fetchdep.sh matrix-org matrix-js-sdk
|
||||
pushd matrix-js-sdk
|
||||
yarn link
|
||||
yarn install
|
||||
popd
|
||||
|
||||
# Now set up the react-sdk
|
||||
yarn link matrix-js-sdk
|
||||
yarn link
|
||||
yarn install
|
||||
|
||||
# Finally, set up element-web
|
||||
scripts/fetchdep.sh vector-im element-web
|
||||
pushd element-web
|
||||
yarn link matrix-js-sdk
|
||||
yarn link matrix-react-sdk
|
||||
yarn install
|
||||
yarn build:res
|
||||
popd
|
|
@ -34,7 +34,7 @@ elif [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
|
|||
fi
|
||||
# Try the target branch of the push or PR.
|
||||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
||||
# Try the current branch from Jenkins.
|
||||
clone $deforg $defrepo `"echo $GIT_BRANCH" | sed -e 's/^origin\///'`
|
||||
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
|
||||
clone $deforg $defrepo $HEAD
|
||||
# Use the default branch as the last resort.
|
||||
clone $deforg $defrepo $defbranch
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {encodeUnpaddedBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
|
||||
import {ActionPayload} from "./dispatcher/payloads";
|
||||
|
@ -25,6 +26,7 @@ import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
|||
import {Action} from "./dispatcher/actions";
|
||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import {idbLoad, idbSave, idbDelete} from "./utils/StorageManager";
|
||||
|
||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
||||
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
||||
|
@ -248,15 +250,16 @@ export default abstract class BasePlatform {
|
|||
* @param {MatrixClient} mxClient the matrix client using which we should start the flow
|
||||
* @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO.
|
||||
* @param {string} fragmentAfterLogin the hash to pass to the app during sso callback.
|
||||
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
|
||||
*/
|
||||
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) {
|
||||
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string, idpId?: string) {
|
||||
// persist hs url and is url for when the user is returned to the app with the login token
|
||||
localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
|
||||
if (mxClient.getIdentityServerUrl()) {
|
||||
localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
|
||||
}
|
||||
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO
|
||||
}
|
||||
|
||||
onKeyDown(ev: KeyboardEvent): boolean {
|
||||
|
@ -272,7 +275,40 @@ export default abstract class BasePlatform {
|
|||
* pickle key has been stored.
|
||||
*/
|
||||
async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
|
||||
return null;
|
||||
if (!window.crypto || !window.crypto.subtle) {
|
||||
return null;
|
||||
}
|
||||
let data;
|
||||
try {
|
||||
data = await idbLoad("pickleKey", [userId, deviceId]);
|
||||
} catch (e) {}
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
if (!data.encrypted || !data.iv || !data.cryptoKey) {
|
||||
console.error("Badly formatted pickle key");
|
||||
return null;
|
||||
}
|
||||
|
||||
const additionalData = new Uint8Array(userId.length + deviceId.length + 1);
|
||||
for (let i = 0; i < userId.length; i++) {
|
||||
additionalData[i] = userId.charCodeAt(i);
|
||||
}
|
||||
additionalData[userId.length] = 124; // "|"
|
||||
for (let i = 0; i < deviceId.length; i++) {
|
||||
additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i);
|
||||
}
|
||||
|
||||
try {
|
||||
const key = await crypto.subtle.decrypt(
|
||||
{name: "AES-GCM", iv: data.iv, additionalData}, data.cryptoKey,
|
||||
data.encrypted,
|
||||
);
|
||||
return encodeUnpaddedBase64(key);
|
||||
} catch (e) {
|
||||
console.error("Error decrypting pickle key");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -283,7 +319,37 @@ export default abstract class BasePlatform {
|
|||
* support storing pickle keys.
|
||||
*/
|
||||
async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
|
||||
return null;
|
||||
if (!window.crypto || !window.crypto.subtle) {
|
||||
return null;
|
||||
}
|
||||
const crypto = window.crypto;
|
||||
const randomArray = new Uint8Array(32);
|
||||
crypto.getRandomValues(randomArray);
|
||||
const cryptoKey = await crypto.subtle.generateKey(
|
||||
{name: "AES-GCM", length: 256}, false, ["encrypt", "decrypt"],
|
||||
);
|
||||
const iv = new Uint8Array(32);
|
||||
crypto.getRandomValues(iv);
|
||||
|
||||
const additionalData = new Uint8Array(userId.length + deviceId.length + 1);
|
||||
for (let i = 0; i < userId.length; i++) {
|
||||
additionalData[i] = userId.charCodeAt(i);
|
||||
}
|
||||
additionalData[userId.length] = 124; // "|"
|
||||
for (let i = 0; i < deviceId.length; i++) {
|
||||
additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i);
|
||||
}
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{name: "AES-GCM", iv, additionalData}, cryptoKey, randomArray,
|
||||
);
|
||||
|
||||
try {
|
||||
await idbSave("pickleKey", [userId, deviceId], {encrypted, iv, cryptoKey});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return encodeUnpaddedBase64(randomArray);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -292,5 +358,8 @@ export default abstract class BasePlatform {
|
|||
* @param {string} userId the device ID that the pickle key is for.
|
||||
*/
|
||||
async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
|
||||
try {
|
||||
await idbDelete("pickleKey", [userId, deviceId]);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,8 @@ import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType }
|
|||
import Analytics from './Analytics';
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
|
@ -114,8 +116,9 @@ function getRemoteAudioElement(): HTMLAudioElement {
|
|||
}
|
||||
|
||||
export default class CallHandler {
|
||||
private calls = new Map<string, MatrixCall>();
|
||||
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||
private dispatcherRef: string = null;
|
||||
|
||||
static sharedInstance() {
|
||||
if (!window.mxCallHandler) {
|
||||
|
@ -126,7 +129,7 @@ export default class CallHandler {
|
|||
}
|
||||
|
||||
start() {
|
||||
dis.register(this.onAction);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
// add empty handlers for media actions, otherwise the media keys
|
||||
// end up causing the audio elements with our ring/ringback etc
|
||||
// audio clips in to play.
|
||||
|
@ -149,6 +152,10 @@ export default class CallHandler {
|
|||
if (cli) {
|
||||
cli.removeListener('Call.incoming', this.onCallIncoming);
|
||||
}
|
||||
if (this.dispatcherRef !== null) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.dispatcherRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
private onCallIncoming = (call) => {
|
||||
|
@ -174,6 +181,28 @@ export default class CallHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
getAllActiveCalls() {
|
||||
const activeCalls = [];
|
||||
|
||||
for (const call of this.calls.values()) {
|
||||
if (call.state !== CallState.Ended && call.state !== CallState.Ringing) {
|
||||
activeCalls.push(call);
|
||||
}
|
||||
}
|
||||
return activeCalls;
|
||||
}
|
||||
|
||||
getAllActiveCallsNotInRoom(notInThisRoomId) {
|
||||
const callsNotInThatRoom = [];
|
||||
|
||||
for (const [roomId, call] of this.calls.entries()) {
|
||||
if (roomId !== notInThisRoomId && call.state !== CallState.Ended) {
|
||||
callsNotInThatRoom.push(call);
|
||||
}
|
||||
}
|
||||
return callsNotInThatRoom;
|
||||
}
|
||||
|
||||
play(audioId: AudioID) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
|
@ -226,11 +255,17 @@ export default class CallHandler {
|
|||
}
|
||||
|
||||
private setCallListeners(call: MatrixCall) {
|
||||
call.on(CallEvent.Error, (err) => {
|
||||
call.on(CallEvent.Error, (err: CallError) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
Analytics.trackEvent('voip', 'callError', 'error', err);
|
||||
Analytics.trackEvent('voip', 'callError', 'error', err.toString());
|
||||
console.error("Call error:", err);
|
||||
|
||||
if (err.code === CallErrorCode.NoUserMedia) {
|
||||
this.showMediaCaptureError(call);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
||||
|
@ -299,8 +334,9 @@ export default class CallHandler {
|
|||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||
title, description,
|
||||
});
|
||||
} else if (call.hangupReason === CallErrorCode.AnsweredElsewhere) {
|
||||
this.play(AudioID.Busy);
|
||||
} else if (
|
||||
call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
|
||||
) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||
title: _t("Answered Elsewhere"),
|
||||
description: _t("The call was answered on another device."),
|
||||
|
@ -377,6 +413,34 @@ export default class CallHandler {
|
|||
}, null, true);
|
||||
}
|
||||
|
||||
private showMediaCaptureError(call: MatrixCall) {
|
||||
let title;
|
||||
let description;
|
||||
|
||||
if (call.type === CallType.Voice) {
|
||||
title = _t("Unable to access microphone");
|
||||
description = <div>
|
||||
{_t(
|
||||
"Call failed because microphone could not be accessed. " +
|
||||
"Check that a microphone is plugged in and set up correctly.",
|
||||
)}
|
||||
</div>;
|
||||
} else if (call.type === CallType.Video) {
|
||||
title = _t("Unable to access webcam / microphone");
|
||||
description = <div>
|
||||
{_t("Call failed because webcam or microphone could not be accessed. Check that:")}
|
||||
<ul>
|
||||
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
|
||||
<li>{_t("Permission is granted to use the webcam")}</li>
|
||||
<li>{_t("No other application is using the webcam")}</li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Media capture failed', '', ErrorDialog, {
|
||||
title, description,
|
||||
}, null, true);
|
||||
}
|
||||
|
||||
private placeCall(
|
||||
roomId: string, type: PlaceCallType,
|
||||
|
@ -389,6 +453,8 @@ export default class CallHandler {
|
|||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
|
||||
this.setActiveCallRoomId(roomId);
|
||||
|
||||
if (type === PlaceCallType.Voice) {
|
||||
call.placeVoiceCall();
|
||||
} else if (type === 'video') {
|
||||
|
@ -417,14 +483,6 @@ export default class CallHandler {
|
|||
switch (payload.action) {
|
||||
case 'place_call':
|
||||
{
|
||||
if (this.getAnyActiveCall()) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||
title: _t('Existing Call'),
|
||||
description: _t('You are already in a call.'),
|
||||
});
|
||||
return; // don't allow >1 call to be placed.
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, whine.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
|
@ -434,6 +492,15 @@ export default class CallHandler {
|
|||
return;
|
||||
}
|
||||
|
||||
// don't allow > 2 calls to be placed.
|
||||
if (this.getAllActiveCalls().length > 1) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||
title: _t('Too Many Calls'),
|
||||
description: _t("You've reached the maximum number of simultaneous calls."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||
if (!room) {
|
||||
console.error("Room %s does not exist.", payload.room_id);
|
||||
|
@ -477,24 +544,21 @@ export default class CallHandler {
|
|||
break;
|
||||
case 'incoming_call':
|
||||
{
|
||||
if (this.getAnyActiveCall()) {
|
||||
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
||||
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
||||
// in future we could signal a "local busy" as a warning to the caller.
|
||||
// see https://github.com/vector-im/vector-web/issues/1964
|
||||
return;
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, stop here.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const call = payload.call as MatrixCall;
|
||||
|
||||
if (this.getCallForRoom(call.roomId)) {
|
||||
// ignore multiple incoming calls to the same room
|
||||
return;
|
||||
}
|
||||
|
||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||
this.calls.set(call.roomId, call)
|
||||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
}
|
||||
break;
|
||||
case 'hangup':
|
||||
|
@ -507,14 +571,26 @@ export default class CallHandler {
|
|||
} else {
|
||||
this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false);
|
||||
}
|
||||
this.removeCallForRoom(payload.room_id);
|
||||
// don't remove the call yet: let the hangup event handler do it (otherwise it will throw
|
||||
// the hangup event away)
|
||||
break;
|
||||
case 'answer': {
|
||||
if (!this.calls.has(payload.room_id)) {
|
||||
return; // no call to answer
|
||||
}
|
||||
|
||||
if (this.getAllActiveCalls().length > 1) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||
title: _t('Too Many Calls'),
|
||||
description: _t("You've reached the maximum number of simultaneous calls."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const call = this.calls.get(payload.room_id);
|
||||
call.answer();
|
||||
this.setCallAudioElement(call);
|
||||
this.setActiveCallRoomId(payload.room_id);
|
||||
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
|
@ -525,6 +601,21 @@ export default class CallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
setActiveCallRoomId(activeCallRoomId: string) {
|
||||
logger.info("Setting call in room " + activeCallRoomId + " active");
|
||||
|
||||
for (const [roomId, call] of this.calls.entries()) {
|
||||
if (call.state === CallState.Ended) continue;
|
||||
|
||||
if (roomId === activeCallRoomId) {
|
||||
call.setRemoteOnHold(false);
|
||||
} else {
|
||||
logger.info("Holding call in room " + roomId + " because another call is being set active");
|
||||
call.setRemoteOnHold(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async startCallApp(roomId: string, type: string) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
|
|
161
src/Lifecycle.ts
161
src/Lifecycle.ts
|
@ -21,6 +21,7 @@ limitations under the License.
|
|||
import Matrix from 'matrix-js-sdk';
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
|
||||
|
||||
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
|
@ -147,20 +148,13 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||
* Gets the user ID of the persisted session, if one exists. This does not validate
|
||||
* that the user's credentials still work, just that they exist and that a user ID
|
||||
* is associated with them. The session is not loaded.
|
||||
* @returns {String} The persisted session's owner, if an owner exists. Null otherwise.
|
||||
* @returns {[String, bool]} The persisted session's owner and whether the stored
|
||||
* session is for a guest user, if an owner exists. If there is no stored session,
|
||||
* return [null, null].
|
||||
*/
|
||||
export function getStoredSessionOwner(): string {
|
||||
const {hsUrl, userId, accessToken} = getLocalStorageSessionVars();
|
||||
return hsUrl && userId && accessToken ? userId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} True if the stored session is for a guest user or false if it is
|
||||
* for a real user. If there is no stored session, return null.
|
||||
*/
|
||||
export function getStoredSessionIsGuest(): boolean {
|
||||
const sessVars = getLocalStorageSessionVars();
|
||||
return sessVars.hsUrl && sessVars.userId && sessVars.accessToken ? sessVars.isGuest : null;
|
||||
export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
||||
const {hsUrl, userId, hasAccessToken, isGuest} = await getStoredSessionVars();
|
||||
return hsUrl && userId && hasAccessToken ? [userId, isGuest] : [null, null];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -197,8 +191,8 @@ export function attemptTokenLogin(
|
|||
},
|
||||
).then(function(creds) {
|
||||
console.log("Logged in with token");
|
||||
return clearStorage().then(() => {
|
||||
persistCredentialsToLocalStorage(creds);
|
||||
return clearStorage().then(async () => {
|
||||
await persistCredentials(creds);
|
||||
// remember that we just logged in
|
||||
sessionStorage.setItem("mx_fresh_login", String(true));
|
||||
return true;
|
||||
|
@ -276,24 +270,42 @@ function registerAsGuest(
|
|||
});
|
||||
}
|
||||
|
||||
export interface ILocalStorageSession {
|
||||
export interface IStoredSession {
|
||||
hsUrl: string;
|
||||
isUrl: string;
|
||||
accessToken: string;
|
||||
hasAccessToken: boolean;
|
||||
accessToken: string | object;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
isGuest: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about the stored session in localstorage. The session
|
||||
* Retrieves information about the stored session from the browser's storage. The session
|
||||
* may not be valid, as it is not tested for consistency here.
|
||||
* @returns {Object} Information about the session - see implementation for variables.
|
||||
*/
|
||||
export function getLocalStorageSessionVars(): ILocalStorageSession {
|
||||
export async function getStoredSessionVars(): Promise<IStoredSession> {
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
const accessToken = localStorage.getItem("mx_access_token");
|
||||
let accessToken;
|
||||
try {
|
||||
accessToken = await StorageManager.idbLoad("account", "mx_access_token");
|
||||
} catch (e) {}
|
||||
if (!accessToken) {
|
||||
accessToken = localStorage.getItem("mx_access_token");
|
||||
if (accessToken) {
|
||||
try {
|
||||
// try to migrate access token to IndexedDB if we can
|
||||
await StorageManager.idbSave("account", "mx_access_token", accessToken);
|
||||
localStorage.removeItem("mx_access_token");
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
// if we pre-date storing "mx_has_access_token", but we retrieved an access
|
||||
// token, then we should say we have an access token
|
||||
const hasAccessToken =
|
||||
(localStorage.getItem("mx_has_access_token") === "true") || !!accessToken;
|
||||
const userId = localStorage.getItem("mx_user_id");
|
||||
const deviceId = localStorage.getItem("mx_device_id");
|
||||
|
||||
|
@ -305,7 +317,43 @@ export function getLocalStorageSessionVars(): ILocalStorageSession {
|
|||
isGuest = localStorage.getItem("matrix-is-guest") === "true";
|
||||
}
|
||||
|
||||
return {hsUrl, isUrl, accessToken, userId, deviceId, isGuest};
|
||||
return {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest};
|
||||
}
|
||||
|
||||
// The pickle key is a string of unspecified length and format. For AES, we
|
||||
// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
|
||||
// key. The AES key should be zeroed after it is used.
|
||||
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
|
||||
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
|
||||
for (let i = 0; i < pickleKey.length; i++) {
|
||||
pickleKeyBuffer[i] = pickleKey.charCodeAt(i);
|
||||
}
|
||||
const hkdfKey = await window.crypto.subtle.importKey(
|
||||
"raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"],
|
||||
);
|
||||
pickleKeyBuffer.fill(0);
|
||||
return new Uint8Array(await window.crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "HKDF", hash: "SHA-256",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
|
||||
salt: new Uint8Array(32), info: new Uint8Array(0),
|
||||
},
|
||||
hkdfKey,
|
||||
256,
|
||||
));
|
||||
}
|
||||
|
||||
async function abortLogin() {
|
||||
const signOut = await showStorageEvictedDialog();
|
||||
if (signOut) {
|
||||
await clearStorage();
|
||||
// This error feels a bit clunky, but we want to make sure we don't go any
|
||||
// further and instead head back to sign in.
|
||||
throw new AbortLoginAndRebuildStorage(
|
||||
"Aborting login in progress because of storage inconsistency",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// returns a promise which resolves to true if a session is found in
|
||||
|
@ -325,7 +373,11 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
|
|||
return false;
|
||||
}
|
||||
|
||||
const {hsUrl, isUrl, accessToken, userId, deviceId, isGuest} = getLocalStorageSessionVars();
|
||||
const {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest} = await getStoredSessionVars();
|
||||
|
||||
if (hasAccessToken && !accessToken) {
|
||||
abortLogin();
|
||||
}
|
||||
|
||||
if (accessToken && userId && hsUrl) {
|
||||
if (ignoreGuest && isGuest) {
|
||||
|
@ -333,9 +385,15 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
|
|||
return false;
|
||||
}
|
||||
|
||||
let decryptedAccessToken = accessToken;
|
||||
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
|
||||
if (pickleKey) {
|
||||
console.log("Got pickle key");
|
||||
if (typeof accessToken !== "string") {
|
||||
const encrKey = await pickleKeyToAesKey(pickleKey);
|
||||
decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token");
|
||||
encrKey.fill(0);
|
||||
}
|
||||
} else {
|
||||
console.log("No pickle key available");
|
||||
}
|
||||
|
@ -347,7 +405,7 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
|
|||
await doSetLoggedIn({
|
||||
userId: userId,
|
||||
deviceId: deviceId,
|
||||
accessToken: accessToken,
|
||||
accessToken: decryptedAccessToken as string,
|
||||
homeserverUrl: hsUrl,
|
||||
identityServerUrl: isUrl,
|
||||
guest: isGuest,
|
||||
|
@ -486,15 +544,7 @@ async function doSetLoggedIn(
|
|||
// crypto store, we'll be generally confused when handling encrypted data.
|
||||
// Show a modal recommending a full reset of storage.
|
||||
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
|
||||
const signOut = await showStorageEvictedDialog();
|
||||
if (signOut) {
|
||||
await clearStorage();
|
||||
// This error feels a bit clunky, but we want to make sure we don't go any
|
||||
// further and instead head back to sign in.
|
||||
throw new AbortLoginAndRebuildStorage(
|
||||
"Aborting login in progress because of storage inconsistency",
|
||||
);
|
||||
}
|
||||
await abortLogin();
|
||||
}
|
||||
|
||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
||||
|
@ -516,7 +566,7 @@ async function doSetLoggedIn(
|
|||
|
||||
if (localStorage) {
|
||||
try {
|
||||
persistCredentialsToLocalStorage(credentials);
|
||||
await persistCredentials(credentials);
|
||||
// make sure we don't think that it's a fresh login any more
|
||||
sessionStorage.removeItem("mx_fresh_login");
|
||||
} catch (e) {
|
||||
|
@ -545,18 +595,55 @@ function showStorageEvictedDialog(): Promise<boolean> {
|
|||
// `instanceof`. Babel 7 supports this natively in their class handling.
|
||||
class AbortLoginAndRebuildStorage extends Error { }
|
||||
|
||||
function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void {
|
||||
async function persistCredentials(credentials: IMatrixClientCreds): Promise<void> {
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
|
||||
if (credentials.identityServerUrl) {
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
|
||||
}
|
||||
localStorage.setItem("mx_user_id", credentials.userId);
|
||||
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
|
||||
|
||||
// store whether we expect to find an access token, to detect the case
|
||||
// where IndexedDB is blown away
|
||||
if (credentials.accessToken) {
|
||||
localStorage.setItem("mx_has_access_token", "true");
|
||||
} else {
|
||||
localStorage.deleteItem("mx_has_access_token");
|
||||
}
|
||||
|
||||
if (credentials.pickleKey) {
|
||||
let encryptedAccessToken;
|
||||
try {
|
||||
// try to encrypt the access token using the pickle key
|
||||
const encrKey = await pickleKeyToAesKey(credentials.pickleKey);
|
||||
encryptedAccessToken = await encryptAES(credentials.accessToken, encrKey, "access_token");
|
||||
encrKey.fill(0);
|
||||
} catch (e) {
|
||||
console.warn("Could not encrypt access token", e);
|
||||
}
|
||||
try {
|
||||
// save either the encrypted access token, or the plain access
|
||||
// token if we were unable to encrypt (e.g. if the browser doesn't
|
||||
// have WebCrypto).
|
||||
await StorageManager.idbSave(
|
||||
"account", "mx_access_token",
|
||||
encryptedAccessToken || credentials.accessToken,
|
||||
);
|
||||
} catch (e) {
|
||||
// if we couldn't save to indexedDB, fall back to localStorage. We
|
||||
// store the access token unencrypted since localStorage only saves
|
||||
// strings.
|
||||
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
}
|
||||
localStorage.setItem("mx_has_pickle_key", String(true));
|
||||
} else {
|
||||
try {
|
||||
await StorageManager.idbSave(
|
||||
"account", "mx_access_token", credentials.accessToken,
|
||||
);
|
||||
} catch (e) {
|
||||
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
}
|
||||
if (localStorage.getItem("mx_has_pickle_key")) {
|
||||
console.error("Expected a pickle key, but none provided. Encryption may not work.");
|
||||
}
|
||||
|
@ -733,6 +820,10 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
|
|||
|
||||
window.localStorage.clear();
|
||||
|
||||
try {
|
||||
await StorageManager.idbDelete("account", "mx_access_token");
|
||||
} catch (e) {}
|
||||
|
||||
// now restore those invites
|
||||
if (!opts?.deleteEverything) {
|
||||
pendingInvites.forEach(i => {
|
||||
|
|
37
src/Login.ts
37
src/Login.ts
|
@ -29,10 +29,23 @@ interface ILoginOptions {
|
|||
}
|
||||
|
||||
// TODO: Move this to JS SDK
|
||||
interface ILoginFlow {
|
||||
type: string;
|
||||
interface IPasswordFlow {
|
||||
type: "m.login.password";
|
||||
}
|
||||
|
||||
export interface IIdentityProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface ISSOFlow {
|
||||
type: "m.login.sso" | "m.login.cas";
|
||||
"org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
|
||||
}
|
||||
|
||||
export type LoginFlow = ISSOFlow | IPasswordFlow;
|
||||
|
||||
// TODO: Move this to JS SDK
|
||||
/* eslint-disable camelcase */
|
||||
interface ILoginParams {
|
||||
|
@ -48,9 +61,8 @@ export default class Login {
|
|||
private hsUrl: string;
|
||||
private isUrl: string;
|
||||
private fallbackHsUrl: string;
|
||||
private currentFlowIndex: number;
|
||||
// TODO: Flows need a type in JS SDK
|
||||
private flows: Array<ILoginFlow>;
|
||||
private flows: Array<LoginFlow>;
|
||||
private defaultDeviceDisplayName: string;
|
||||
private tempClient: MatrixClient;
|
||||
|
||||
|
@ -63,7 +75,6 @@ export default class Login {
|
|||
this.hsUrl = hsUrl;
|
||||
this.isUrl = isUrl;
|
||||
this.fallbackHsUrl = fallbackHsUrl;
|
||||
this.currentFlowIndex = 0;
|
||||
this.flows = [];
|
||||
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
this.tempClient = null; // memoize
|
||||
|
@ -100,27 +111,13 @@ export default class Login {
|
|||
});
|
||||
}
|
||||
|
||||
public async getFlows(): Promise<Array<ILoginFlow>> {
|
||||
public async getFlows(): Promise<Array<LoginFlow>> {
|
||||
const client = this.createTemporaryClient();
|
||||
const { flows } = await client.loginFlows();
|
||||
this.flows = flows;
|
||||
this.currentFlowIndex = 0;
|
||||
// technically the UI should display options for all flows for the
|
||||
// user to then choose one, so return all the flows here.
|
||||
return this.flows;
|
||||
}
|
||||
|
||||
public chooseFlow(flowIndex): void {
|
||||
this.currentFlowIndex = flowIndex;
|
||||
}
|
||||
|
||||
public getCurrentFlowStep(): string {
|
||||
// technically the flow can have multiple steps, but no one does this
|
||||
// for login so we can ignore it.
|
||||
const flowStep = this.flows[this.currentFlowIndex];
|
||||
return flowStep ? flowStep.type : null;
|
||||
}
|
||||
|
||||
public loginViaPassword(
|
||||
username: string,
|
||||
phoneCountry: string,
|
||||
|
|
|
@ -40,10 +40,6 @@ export default class PasswordReset {
|
|||
this.identityServerDomain = identityUrl ? identityUrl.split("://")[1] : null;
|
||||
}
|
||||
|
||||
doesServerRequireIdServerParam() {
|
||||
return this.client.doesServerRequireIdServerParam();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to reset the user's password. This will trigger a side-effect of
|
||||
* sending an email to the provided email address.
|
||||
|
@ -78,9 +74,6 @@ export default class PasswordReset {
|
|||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
};
|
||||
if (await this.doesServerRequireIdServerParam()) {
|
||||
creds.id_server = this.identityServerDomain;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.setPassword({
|
||||
|
|
|
@ -46,6 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
|
|||
import SdkConfig from "./SdkConfig";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
import {CHAT_EFFECTS} from "./effects"
|
||||
import CallHandler from "./CallHandler";
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
|
@ -78,6 +79,7 @@ export const CommandCategories = {
|
|||
"actions": _td("Actions"),
|
||||
"admin": _td("Admin"),
|
||||
"advanced": _td("Advanced"),
|
||||
"effects": _td("Effects"),
|
||||
"other": _td("Other"),
|
||||
};
|
||||
|
||||
|
@ -1094,6 +1096,30 @@ export const Commands = [
|
|||
category: CommandCategories.messages,
|
||||
hideCompletionAfterSpace: true,
|
||||
}),
|
||||
|
||||
...CHAT_EFFECTS.map((effect) => {
|
||||
return new Command({
|
||||
command: effect.command,
|
||||
description: effect.description(),
|
||||
args: '<message>',
|
||||
runFn: function(roomId, args) {
|
||||
return success((async () => {
|
||||
if (!args) {
|
||||
args = effect.fallbackMessage();
|
||||
MatrixClientPeg.get().sendEmoteMessage(roomId, args);
|
||||
} else {
|
||||
const content = {
|
||||
msgtype: effect.msgType,
|
||||
body: args,
|
||||
};
|
||||
MatrixClientPeg.get().sendMessage(roomId, content);
|
||||
}
|
||||
dis.dispatch({action: `effects.${effect.command}`});
|
||||
})());
|
||||
},
|
||||
category: CommandCategories.effects,
|
||||
})
|
||||
}),
|
||||
];
|
||||
|
||||
// build a map from names and aliases to the Command objects.
|
||||
|
|
|
@ -398,7 +398,7 @@ export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
|||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => {
|
||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonRight = elementRect.right + window.pageXOffset;
|
||||
|
@ -408,9 +408,9 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
|||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
menuOptions.top = buttonBottom;
|
||||
menuOptions.top = buttonBottom + vPadding;
|
||||
} else {
|
||||
menuOptions.bottom = window.innerHeight - buttonTop;
|
||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||
}
|
||||
|
||||
return menuOptions;
|
||||
|
|
|
@ -34,7 +34,6 @@ import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
|||
import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import * as RoomListSorter from "../../RoomListSorter";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Notifier from '../../Notifier';
|
||||
|
||||
|
@ -48,7 +47,6 @@ import * as Lifecycle from '../../Lifecycle';
|
|||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
import '../../stores/LifecycleStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import { getHomePageUrl } from '../../utils/pages';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||
|
@ -591,7 +589,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
MatrixClientPeg.get().leave(payload.room_id).then(() => {
|
||||
modal.close();
|
||||
if (this.state.currentRoomId === payload.room_id) {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
}, (err) => {
|
||||
modal.close();
|
||||
|
@ -620,9 +618,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'view_next_room':
|
||||
this.viewNextRoom(1);
|
||||
break;
|
||||
case Action.ViewUserSettings: {
|
||||
const tabPayload = payload as OpenToTabPayload;
|
||||
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
|
||||
|
@ -802,35 +797,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.notifyNewScreen('register');
|
||||
}
|
||||
|
||||
// TODO: Move to RoomViewStore
|
||||
private viewNextRoom(roomIndexDelta: number) {
|
||||
const allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||
MatrixClientPeg.get().getRooms(),
|
||||
);
|
||||
// If there are 0 rooms or 1 room, view the home page because otherwise
|
||||
// if there are 0, we end up trying to index into an empty array, and
|
||||
// if there is 1, we end up viewing the same room.
|
||||
if (allRooms.length < 2) {
|
||||
dis.dispatch({
|
||||
action: 'view_home_page',
|
||||
});
|
||||
return;
|
||||
}
|
||||
let roomIndex = -1;
|
||||
for (let i = 0; i < allRooms.length; ++i) {
|
||||
if (allRooms[i].roomId === this.state.currentRoomId) {
|
||||
roomIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
||||
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: allRooms[roomIndex].roomId,
|
||||
});
|
||||
}
|
||||
|
||||
// switch view to the given room
|
||||
//
|
||||
// @param {Object} roomInfo Object containing data about the room to be joined
|
||||
|
@ -1097,9 +1063,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private forgetRoom(roomId: string) {
|
||||
MatrixClientPeg.get().forget(roomId).then(() => {
|
||||
// Switch to another room view if we're currently viewing the historical room
|
||||
// Switch to home page if we're currently viewing the forgotten room
|
||||
if (this.state.currentRoomId === roomId) {
|
||||
dis.dispatch({ action: "view_next_room" });
|
||||
dis.dispatch({ action: "view_home_page" });
|
||||
}
|
||||
}).catch((err) => {
|
||||
const errCode = err.errcode || _td("unknown error code");
|
||||
|
@ -1233,12 +1199,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
} else {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'view_welcome_page'});
|
||||
} else if (getHomePageUrl(this.props.config)) {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else {
|
||||
this.firstSyncPromise.promise.then(() => {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
});
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2009,6 +1971,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
onLoginClick={this.onLoginClick}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
{...this.getServerProperties()}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -69,11 +69,15 @@ import AuxPanel from "../views/rooms/AuxPanel";
|
|||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import {XOR} from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import {containsEmoji} from '../../effects/utils';
|
||||
import {CHAT_EFFECTS} from '../../effects';
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -248,6 +252,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
this.context.on("Event.decrypted", this.onEventDecrypted);
|
||||
this.context.on("event", this.onEvent);
|
||||
// Start listening for RoomViewStore updates
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||
|
@ -581,6 +587,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
this.context.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
this.context.removeListener("event", this.onEvent);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
|
@ -781,6 +789,30 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onEventDecrypted = (ev) => {
|
||||
if (ev.isDecryptionFailure()) return;
|
||||
this.handleEffects(ev);
|
||||
};
|
||||
|
||||
private onEvent = (ev) => {
|
||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
||||
this.handleEffects(ev);
|
||||
};
|
||||
|
||||
private handleEffects = (ev) => {
|
||||
if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
|
||||
if (ev.getRoomId() !== this.state.room.roomId) return; // not for us
|
||||
|
||||
const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
|
||||
if (!notifState.isUnread) return;
|
||||
|
||||
CHAT_EFFECTS.forEach(effect => {
|
||||
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
|
||||
dis.dispatch({action: `effects.${effect.command}`});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private onRoomName = (room: Room) => {
|
||||
if (this.state.room && room.roomId == this.state.room.roomId) {
|
||||
this.forceUpdate();
|
||||
|
@ -1332,7 +1364,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
rejecting: true,
|
||||
});
|
||||
this.context.leave(this.state.roomId).then(() => {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
dis.dispatch({ action: 'view_home_page' });
|
||||
this.setState({
|
||||
rejecting: false,
|
||||
});
|
||||
|
@ -1366,7 +1398,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
await this.context.setIgnoredUsers(ignoredUsers);
|
||||
|
||||
await this.context.leave(this.state.roomId);
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
dis.dispatch({ action: 'view_home_page' });
|
||||
this.setState({
|
||||
rejecting: false,
|
||||
});
|
||||
|
@ -1946,9 +1978,14 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
mx_RoomView_inCall: Boolean(activeCall),
|
||||
});
|
||||
|
||||
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
{showChatEffects && this.roomView.current &&
|
||||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||
}
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={this.state.room}
|
||||
|
|
|
@ -715,26 +715,22 @@ class TimelinePanel extends React.Component {
|
|||
}
|
||||
this.lastRMSentEventId = this.state.readMarkerEventId;
|
||||
|
||||
const roomId = this.props.timelineSet.room.roomId;
|
||||
const hiddenRR = !SettingsStore.getValue("sendReadReceipts", roomId);
|
||||
|
||||
debuglog('TimelinePanel: Sending Read Markers for ',
|
||||
this.props.timelineSet.room.roomId,
|
||||
'rm', this.state.readMarkerEventId,
|
||||
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
||||
' hidden:' + hiddenRR,
|
||||
);
|
||||
MatrixClientPeg.get().setRoomReadMarkers(
|
||||
this.props.timelineSet.room.roomId,
|
||||
this.state.readMarkerEventId,
|
||||
lastReadEvent, // Could be null, in which case no RR is sent
|
||||
{hidden: hiddenRR},
|
||||
{},
|
||||
).catch((e) => {
|
||||
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
||||
return MatrixClientPeg.get().sendReadReceipt(
|
||||
lastReadEvent,
|
||||
{hidden: hiddenRR},
|
||||
{},
|
||||
).catch((e) => {
|
||||
console.error(e);
|
||||
this.lastRRSentEventId = undefined;
|
||||
|
|
|
@ -21,16 +21,14 @@ import PropTypes from 'prop-types';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from 'classnames';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
const PHASE_SERVER_DETAILS = 0;
|
||||
// Show the forgot password inputs
|
||||
const PHASE_FORGOT = 1;
|
||||
// Email is in the process of being sent
|
||||
|
@ -62,7 +60,6 @@ export default class ForgotPassword extends React.Component {
|
|||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
serverRequiresIdServer: null,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -93,12 +90,8 @@ export default class ForgotPassword extends React.Component {
|
|||
serverConfig.isUrl,
|
||||
);
|
||||
|
||||
const pwReset = new PasswordReset(serverConfig.hsUrl, serverConfig.isUrl);
|
||||
const serverRequiresIdServer = await pwReset.doesServerRequireIdServerParam();
|
||||
|
||||
this.setState({
|
||||
serverIsAlive: true,
|
||||
serverRequiresIdServer,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password"));
|
||||
|
@ -177,20 +170,6 @@ export default class ForgotPassword extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onServerDetailsNextPhaseClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_FORGOT,
|
||||
});
|
||||
};
|
||||
|
||||
onEditServerDetailsClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
};
|
||||
|
||||
onLoginClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -205,24 +184,6 @@ export default class ForgotPassword extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
renderServerDetails() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ServerConfig
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={0}
|
||||
showIdentityServerIfRequiredByHomeserver={true}
|
||||
onAfterSubmit={this.onServerDetailsNextPhaseClick}
|
||||
submitText={_t("Next")}
|
||||
submitClass="mx_Login_submit"
|
||||
/>;
|
||||
}
|
||||
|
||||
renderForgot() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
|
@ -246,57 +207,13 @@ export default class ForgotPassword extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
yourMatrixAccountText = _t('Your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
let editLink = null;
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
if (!this.props.serverConfig.isUrl && this.state.serverRequiresIdServer) {
|
||||
return <div>
|
||||
<h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3>
|
||||
{_t(
|
||||
"No identity server is configured: " +
|
||||
"add one in server settings to reset your password.",
|
||||
)}
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{_t('Sign in instead')}
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
{errorText}
|
||||
{serverDeadSection}
|
||||
<h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3>
|
||||
<ServerPicker
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
/>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
|
@ -319,6 +236,7 @@ export default class ForgotPassword extends React.Component {
|
|||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Field
|
||||
name="reset_password_confirm"
|
||||
|
@ -328,6 +246,7 @@ export default class ForgotPassword extends React.Component {
|
|||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<span>{_t(
|
||||
|
@ -380,9 +299,6 @@ export default class ForgotPassword extends React.Component {
|
|||
|
||||
let resetPasswordJsx;
|
||||
switch (this.state.phase) {
|
||||
case PHASE_SERVER_DETAILS:
|
||||
resetPasswordJsx = this.renderServerDetails();
|
||||
break;
|
||||
case PHASE_FORGOT:
|
||||
resetPasswordJsx = this.renderForgot();
|
||||
break;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016, 2017, 2018, 2019 New Vector Ltd
|
||||
Copyright 2015, 2016, 2017, 2018, 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,30 +14,27 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {ComponentProps, ReactNode} from 'react';
|
||||
import React, {ReactNode} from 'react';
|
||||
import {MatrixError} from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import Login, {ISSOFlow, LoginFlow} from '../../../Login';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {IMatrixClientCreds} from "../../../MatrixClientPeg";
|
||||
import ServerConfig from "../../views/auth/ServerConfig";
|
||||
import PasswordLogin from "../../views/auth/PasswordLogin";
|
||||
import SignInToText from "../../views/auth/SignInToText";
|
||||
import InlineSpinner from "../../views/elements/InlineSpinner";
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
|
||||
// Enable phases for login
|
||||
const PHASES_ENABLED = true;
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
|
||||
// These are used in several places, and come from the js-sdk's autodiscovery
|
||||
// stuff. We define them here so that they'll be picked up by i18n.
|
||||
|
@ -75,13 +72,6 @@ interface IProps {
|
|||
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
// Show controls to configure server details
|
||||
ServerDetails,
|
||||
// Show the appropriate login flow(s) for the server
|
||||
Login,
|
||||
}
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
busyLoggingIn?: boolean;
|
||||
|
@ -90,17 +80,13 @@ interface IState {
|
|||
// can we attempt to log in or are there validation errors?
|
||||
canTryLogin: boolean;
|
||||
|
||||
flows?: LoginFlow[];
|
||||
|
||||
// used for preserving form values when changing homeserver
|
||||
username: string;
|
||||
phoneCountry?: string;
|
||||
phoneNumber: string;
|
||||
|
||||
// Phase of the overall login dialog.
|
||||
phase: Phase;
|
||||
// The current login flow, such as password, SSO, etc.
|
||||
// we need to load the flows from the server
|
||||
currentFlow?: string;
|
||||
|
||||
// We perform liveliness checks later, but for now suppress the errors.
|
||||
// We also track the server dead errors independently of the regular errors so
|
||||
// that we can render it differently, and override any other error the user may
|
||||
|
@ -113,9 +99,10 @@ interface IState {
|
|||
/*
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
*/
|
||||
export default class LoginComponent extends React.Component<IProps, IState> {
|
||||
export default class LoginComponent extends React.PureComponent<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private loginLogic: Login;
|
||||
|
||||
private readonly stepRendererMap: Record<string, () => ReactNode>;
|
||||
|
||||
constructor(props) {
|
||||
|
@ -127,11 +114,13 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
canTryLogin: true,
|
||||
|
||||
flows: null,
|
||||
|
||||
username: "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
phase: Phase.Login,
|
||||
currentFlow: null,
|
||||
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
|
@ -351,13 +340,15 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
onTryRegisterClick = ev => {
|
||||
const step = this.getCurrentFlowStep();
|
||||
if (step === 'm.login.sso' || step === 'm.login.cas') {
|
||||
// If we're showing SSO it means that registration is also probably disabled,
|
||||
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
|
||||
const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
|
||||
const ssoFlow = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
|
||||
// If has no password flow but an SSO flow guess that the user wants to register with SSO.
|
||||
// TODO: instead hide the Register button if registration is disabled by checking with the server,
|
||||
// has no specific errCode currently and uses M_FORBIDDEN.
|
||||
if (ssoFlow && !hasPasswordFlow) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
|
||||
const ssoKind = ssoFlow.type === 'm.login.sso' ? 'sso' : 'cas';
|
||||
PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind,
|
||||
this.props.fragmentAfterLogin);
|
||||
} else {
|
||||
|
@ -366,20 +357,6 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onServerDetailsNextPhaseClick = () => {
|
||||
this.setState({
|
||||
phase: Phase.Login,
|
||||
});
|
||||
};
|
||||
|
||||
private onEditServerDetailsClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: Phase.ServerDetails,
|
||||
});
|
||||
};
|
||||
|
||||
private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
|
||||
let isDefaultServer = false;
|
||||
if (this.props.serverConfig.isDefault
|
||||
|
@ -397,7 +374,6 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
|
||||
this.setState({
|
||||
busy: true,
|
||||
currentFlow: null, // reset flow
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
|
@ -421,38 +397,22 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
busy: false,
|
||||
...AutoDiscoveryUtils.authComponentStateForError(e),
|
||||
});
|
||||
if (this.state.serverErrorIsFatal) {
|
||||
// Server is dead: show server details prompt instead
|
||||
this.setState({
|
||||
phase: Phase.ServerDetails,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loginLogic.getFlows().then((flows) => {
|
||||
// look for a flow where we understand all of the steps.
|
||||
for (let i = 0; i < flows.length; i++ ) {
|
||||
if (!this.isSupportedFlow(flows[i])) {
|
||||
continue;
|
||||
}
|
||||
const supportedFlows = flows.filter(this.isSupportedFlow);
|
||||
|
||||
// we just pick the first flow where we support all the
|
||||
// steps. (we don't have a UI for multiple logins so let's skip
|
||||
// that for now).
|
||||
loginLogic.chooseFlow(i);
|
||||
if (supportedFlows.length > 0) {
|
||||
this.setState({
|
||||
currentFlow: this.getCurrentFlowStep(),
|
||||
flows: supportedFlows,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// we got to the end of the list without finding a suitable
|
||||
// flow.
|
||||
|
||||
// we got to the end of the list without finding a suitable flow.
|
||||
this.setState({
|
||||
errorText: _t(
|
||||
"This homeserver doesn't offer any login flows which are " +
|
||||
"supported by this client.",
|
||||
),
|
||||
errorText: _t("This homeserver doesn't offer any login flows which are supported by this client."),
|
||||
});
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
|
@ -467,7 +427,7 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private isSupportedFlow(flow) {
|
||||
private isSupportedFlow = (flow: LoginFlow): boolean => {
|
||||
// technically the flow can have multiple steps, but no one does this
|
||||
// for login and loginLogic doesn't support it so we can ignore it.
|
||||
if (!this.stepRendererMap[flow.type]) {
|
||||
|
@ -475,20 +435,16 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private getCurrentFlowStep() {
|
||||
return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null;
|
||||
}
|
||||
|
||||
private errorTextFromError(err) {
|
||||
private errorTextFromError(err: MatrixError): ReactNode {
|
||||
let errCode = err.errcode;
|
||||
if (!errCode && err.httpStatus) {
|
||||
errCode = "HTTP " + err.httpStatus;
|
||||
}
|
||||
|
||||
let errorText: ReactNode = _t("Error: Problem communicating with the given homeserver.") +
|
||||
(errCode ? " (" + errCode + ")" : "");
|
||||
let errorText: ReactNode = _t("There was a problem communicating with the homeserver, " +
|
||||
"please try again later.") + (errCode ? " (" + errCode + ")" : "");
|
||||
|
||||
if (err.cors === 'rejected') {
|
||||
if (window.location.protocol === 'https:' &&
|
||||
|
@ -526,61 +482,28 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
return errorText;
|
||||
}
|
||||
|
||||
private renderServerComponent() {
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
renderLoginComponentForFlows() {
|
||||
if (!this.state.flows) return null;
|
||||
|
||||
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
|
||||
return null;
|
||||
}
|
||||
// this is the ideal order we want to show the flows in
|
||||
const order = [
|
||||
"m.login.password",
|
||||
"m.login.sso",
|
||||
];
|
||||
|
||||
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
|
||||
if (PHASES_ENABLED) {
|
||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||
serverDetailsProps.submitText = _t("Next");
|
||||
serverDetailsProps.submitClass = "mx_Login_submit";
|
||||
}
|
||||
|
||||
return <ServerConfig
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
}
|
||||
|
||||
private renderLoginComponentForStep() {
|
||||
if (PHASES_ENABLED && this.state.phase !== Phase.Login) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const step = this.state.currentFlow;
|
||||
|
||||
if (!step) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepRenderer = this.stepRendererMap[step];
|
||||
|
||||
if (stepRenderer) {
|
||||
return stepRenderer();
|
||||
}
|
||||
|
||||
return null;
|
||||
const flows = order.map(type => this.state.flows.find(flow => flow.type === type)).filter(Boolean);
|
||||
return <React.Fragment>
|
||||
{ flows.map(flow => {
|
||||
const stepRenderer = this.stepRendererMap[flow.type];
|
||||
return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>
|
||||
}) }
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
private renderPasswordStep = () => {
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
username={this.state.username}
|
||||
phoneCountry={this.state.phoneCountry}
|
||||
phoneNumber={this.state.phoneNumber}
|
||||
|
@ -598,31 +521,16 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private renderSsoStep = loginType => {
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
// XXX: This link does *not* have a target="_blank" because single sign-on relies on
|
||||
// redirecting the user back to a URI once they're logged in. On the web, this means
|
||||
// we use the same window and redirect back to Element. On Electron, this actually
|
||||
// opens the SSO page in the Electron app itself due to
|
||||
// https://github.com/electron/electron/issues/8841 and so happens to work.
|
||||
// If this bug gets fixed, it will break SSO since it will open the SSO page in the
|
||||
// user's browser, let them log into their SSO provider, then redirect their browser
|
||||
// to vector://vector which, of course, will not work.
|
||||
return (
|
||||
<div>
|
||||
<SignInToText serverConfig={this.props.serverConfig}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick} />
|
||||
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
|
||||
|
||||
<SSOButton
|
||||
className="mx_Login_sso_link mx_Login_submit"
|
||||
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<SSOButtons
|
||||
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||
flow={flow}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -670,9 +578,11 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
</div>;
|
||||
} else if (SettingsStore.getValue(UIFeature.Registration)) {
|
||||
footer = (
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
|
||||
{ _t('Create account') }
|
||||
</a>
|
||||
<span className="mx_AuthBody_changeFlow">
|
||||
{_t("New? <a>Create account</a>", {}, {
|
||||
a: sub => <a onClick={this.onTryRegisterClick} href="#">{ sub }</a>,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -686,8 +596,11 @@ export default class LoginComponent extends React.Component<IProps, IState> {
|
|||
</h2>
|
||||
{ errorTextSection }
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderLoginComponentForStep() }
|
||||
<ServerPicker
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
/>
|
||||
{ this.renderLoginComponentForFlows() }
|
||||
{ footer }
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
|
|
|
@ -15,29 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import React, {ComponentProps, ReactNode} from 'react';
|
||||
import React, {ReactNode} from 'react';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import Login from "../../../Login";
|
||||
import Login, {ISSOFlow} from "../../../Login";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
// Phases
|
||||
enum Phase {
|
||||
// Show controls to configure server details
|
||||
ServerDetails = 0,
|
||||
// Show the appropriate registration flow(s) for the server
|
||||
Registration = 1,
|
||||
}
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
import ServerPicker from '../../views/elements/ServerPicker';
|
||||
|
||||
interface IProps {
|
||||
serverConfig: ValidatedServerConfig;
|
||||
|
@ -47,6 +39,7 @@ interface IProps {
|
|||
clientSecret?: string;
|
||||
sessionId?: string;
|
||||
idSid?: string;
|
||||
fragmentAfterLogin?: string;
|
||||
|
||||
// Called when the user has logged in. Params:
|
||||
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
||||
|
@ -92,9 +85,6 @@ interface IState {
|
|||
// If set, we've registered but are not going to log
|
||||
// the user in to their new account automatically.
|
||||
completedNoSignin: boolean;
|
||||
serverType: ServerType.FREE | ServerType.PREMIUM | ServerType.ADVANCED;
|
||||
// Phase of the overall registration dialog.
|
||||
phase: Phase;
|
||||
flows: {
|
||||
stages: string[];
|
||||
}[];
|
||||
|
@ -109,23 +99,22 @@ interface IState {
|
|||
// Our matrix client - part of state because we can't render the UI auth
|
||||
// component without it.
|
||||
matrixClient?: MatrixClient;
|
||||
// whether the HS requires an ID server to register with a threepid
|
||||
serverRequiresIdServer?: boolean;
|
||||
// The user ID we've just registered
|
||||
registeredUsername?: string;
|
||||
// if a different user ID to the one we just registered is logged in,
|
||||
// this is the user ID that's logged in.
|
||||
differentLoggedInUserId?: string;
|
||||
// the SSO flow definition, this is fetched from /login as that's the only
|
||||
// place it is exposed.
|
||||
ssoFlow?: ISSOFlow;
|
||||
}
|
||||
|
||||
// Enable phases for registration
|
||||
const PHASES_ENABLED = true;
|
||||
|
||||
export default class Registration extends React.Component<IProps, IState> {
|
||||
loginLogic: Login;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
|
||||
this.state = {
|
||||
busy: false,
|
||||
errorText: null,
|
||||
|
@ -133,14 +122,17 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
email: this.props.email,
|
||||
},
|
||||
doingUIAuth: Boolean(this.props.sessionId),
|
||||
serverType,
|
||||
phase: Phase.Registration,
|
||||
flows: null,
|
||||
completedNoSignin: false,
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
};
|
||||
|
||||
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||
this.loginLogic = new Login(hsUrl, isUrl, null, {
|
||||
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -154,61 +146,8 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
this.replaceClient(newProps.serverConfig);
|
||||
|
||||
// Handle cases where the user enters "https://matrix.org" for their server
|
||||
// from the advanced option - we should default to FREE at that point.
|
||||
const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig);
|
||||
if (serverType !== this.state.serverType) {
|
||||
// Reset the phase to default phase for the server type.
|
||||
this.setState({
|
||||
serverType,
|
||||
phase: Registration.getDefaultPhaseForServerType(serverType),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static getDefaultPhaseForServerType(type: IState["serverType"]) {
|
||||
switch (type) {
|
||||
case ServerType.FREE: {
|
||||
// Move directly to the registration phase since the server
|
||||
// details are fixed.
|
||||
return Phase.Registration;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
case ServerType.ADVANCED:
|
||||
return Phase.ServerDetails;
|
||||
}
|
||||
}
|
||||
|
||||
private onServerTypeChange = (type: IState["serverType"]) => {
|
||||
this.setState({
|
||||
serverType: type,
|
||||
});
|
||||
|
||||
// When changing server types, set the HS / IS URLs to reasonable defaults for the
|
||||
// the new type.
|
||||
switch (type) {
|
||||
case ServerType.FREE: {
|
||||
const { serverConfig } = ServerType.TYPES.FREE;
|
||||
this.props.onServerConfigChange(serverConfig);
|
||||
break;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
// We can accept whatever server config was the default here as this essentially
|
||||
// acts as a slightly different "custom server"/ADVANCED option.
|
||||
break;
|
||||
case ServerType.ADVANCED:
|
||||
// Use the default config from the config
|
||||
this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]);
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset the phase to default phase for the server type.
|
||||
this.setState({
|
||||
phase: Registration.getDefaultPhaseForServerType(type),
|
||||
});
|
||||
};
|
||||
|
||||
private async replaceClient(serverConfig: ValidatedServerConfig) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
|
@ -245,16 +184,20 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
idBaseUrl: isUrl,
|
||||
});
|
||||
|
||||
let serverRequiresIdServer = true;
|
||||
this.loginLogic.setHomeserverUrl(hsUrl);
|
||||
this.loginLogic.setIdentityServerUrl(isUrl);
|
||||
|
||||
let ssoFlow: ISSOFlow;
|
||||
try {
|
||||
serverRequiresIdServer = await cli.doesServerRequireIdServerParam();
|
||||
const loginFlows = await this.loginLogic.getFlows();
|
||||
ssoFlow = loginFlows.find(f => f.type === "m.login.sso" || f.type === "m.login.cas") as ISSOFlow;
|
||||
} catch (e) {
|
||||
console.log("Unable to determine is server needs id_server param", e);
|
||||
console.error("Failed to get login flows to check for SSO support", e);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
matrixClient: cli,
|
||||
serverRequiresIdServer,
|
||||
ssoFlow,
|
||||
busy: false,
|
||||
});
|
||||
const showGenericError = (e) => {
|
||||
|
@ -282,26 +225,16 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
// At this point registration is pretty much disabled, but before we do that let's
|
||||
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
||||
// the user off to the login page to figure their account out.
|
||||
try {
|
||||
const loginLogic = new Login(hsUrl, isUrl, null, {
|
||||
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
|
||||
if (ssoFlow) {
|
||||
// Redirect to login page - server probably expects SSO only
|
||||
dis.dispatch({action: 'start_login'});
|
||||
} else {
|
||||
this.setState({
|
||||
serverErrorIsFatal: true, // fatal because user cannot continue on this server
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
const flows = await loginLogic.getFlows();
|
||||
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
|
||||
if (hasSsoFlow) {
|
||||
// Redirect to login page - server probably expects SSO only
|
||||
dis.dispatch({action: 'start_login'});
|
||||
} else {
|
||||
this.setState({
|
||||
serverErrorIsFatal: true, // fatal because user cannot continue on this server
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get login flows to check for SSO support", e);
|
||||
showGenericError(e);
|
||||
}
|
||||
} else {
|
||||
console.log("Unable to query for supported registration methods.", e);
|
||||
|
@ -365,6 +298,8 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
if (!msisdnAvailable) {
|
||||
msg = _t('This server does not support authentication with a phone number.');
|
||||
}
|
||||
} else if (response.errcode === "M_USER_IN_USE") {
|
||||
msg = _t("That username already exists, please try another.");
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
|
@ -390,8 +325,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
// isn't a guest user since we'll usually have set a guest user session before
|
||||
// starting the registration process. This isn't perfect since it's possible
|
||||
// the user had a separate guest session they didn't actually mean to replace.
|
||||
const sessionOwner = Lifecycle.getStoredSessionOwner();
|
||||
const sessionIsGuest = Lifecycle.getStoredSessionIsGuest();
|
||||
const [sessionOwner, sessionIsGuest] = await Lifecycle.getStoredSessionOwner();
|
||||
if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) {
|
||||
console.log(
|
||||
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
|
||||
|
@ -453,21 +387,6 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
this.setState({
|
||||
busy: false,
|
||||
doingUIAuth: false,
|
||||
phase: Phase.Registration,
|
||||
});
|
||||
};
|
||||
|
||||
private onServerDetailsNextPhaseClick = async () => {
|
||||
this.setState({
|
||||
phase: Phase.Registration,
|
||||
});
|
||||
};
|
||||
|
||||
private onEditServerDetailsClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: Phase.ServerDetails,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -516,77 +435,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private renderServerComponent() {
|
||||
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hide the server picker once the user is doing UI Auth unless encountered a fatal server error
|
||||
if (this.state.phase !== Phase.ServerDetails && this.state.doingUIAuth && !this.state.serverErrorIsFatal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we're on a different phase, we only show the server type selector,
|
||||
// which is always shown if we allow custom URLs at all.
|
||||
// (if there's a fatal server error, we need to show the full server
|
||||
// config as the user may need to change servers to resolve the error).
|
||||
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
|
||||
if (PHASES_ENABLED) {
|
||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||
serverDetailsProps.submitText = _t("Next");
|
||||
serverDetailsProps.submitClass = "mx_Login_submit";
|
||||
}
|
||||
|
||||
let serverDetails = null;
|
||||
switch (this.state.serverType) {
|
||||
case ServerType.FREE:
|
||||
break;
|
||||
case ServerType.PREMIUM:
|
||||
serverDetails = <ModularServerConfig
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
break;
|
||||
case ServerType.ADVANCED:
|
||||
serverDetails = <ServerConfig
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
showIdentityServerIfRequiredByHomeserver={true}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
{serverDetails}
|
||||
</div>;
|
||||
}
|
||||
|
||||
private renderRegisterComponent() {
|
||||
if (PHASES_ENABLED && this.state.phase !== Phase.Registration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const RegistrationForm = sdk.getComponent('auth.RegistrationForm');
|
||||
|
@ -610,18 +459,47 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
<Spinner />
|
||||
</div>;
|
||||
} else if (this.state.flows.length) {
|
||||
return <RegistrationForm
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
flows={this.state.flows}
|
||||
serverConfig={this.props.serverConfig}
|
||||
canSubmit={!this.state.serverErrorIsFatal}
|
||||
serverRequiresIdServer={this.state.serverRequiresIdServer}
|
||||
/>;
|
||||
let ssoSection;
|
||||
if (this.state.ssoFlow) {
|
||||
let continueWithSection;
|
||||
const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"] || [];
|
||||
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
|
||||
if (providers.length > 1) {
|
||||
// i18n: ssoButtons is a placeholder to help translators understand context
|
||||
continueWithSection = <h3 className="mx_AuthBody_centered">
|
||||
{ _t("Continue with %(ssoButtons)s", { ssoButtons: "" }).trim() }
|
||||
</h3>;
|
||||
}
|
||||
|
||||
// i18n: ssoButtons & usernamePassword are placeholders to help translators understand context
|
||||
ssoSection = <React.Fragment>
|
||||
{ continueWithSection }
|
||||
<SSOButtons
|
||||
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||
flow={this.state.ssoFlow}
|
||||
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
/>
|
||||
<h3 className="mx_AuthBody_centered">
|
||||
{ _t("%(ssoButtons)s Or %(usernamePassword)s", { ssoButtons: "", usernamePassword: ""}).trim() }
|
||||
</h3>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
{ ssoSection }
|
||||
<RegistrationForm
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
flows={this.state.flows}
|
||||
serverConfig={this.props.serverConfig}
|
||||
canSubmit={!this.state.serverErrorIsFatal}
|
||||
/>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -650,13 +528,15 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
const signIn = <a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{ _t('Sign in instead') }
|
||||
</a>;
|
||||
const signIn = <span className="mx_AuthBody_changeFlow">
|
||||
{_t("Already have an account? <a>Sign in here</a>", {}, {
|
||||
a: sub => <a onClick={this.onLoginClick} href="#">{ sub }</a>,
|
||||
})}
|
||||
</span>;
|
||||
|
||||
// Only show the 'go back' button if you're not looking at the form
|
||||
let goBack;
|
||||
if ((PHASES_ENABLED && this.state.phase !== Phase.Registration) || this.state.doingUIAuth) {
|
||||
if (this.state.doingUIAuth) {
|
||||
goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
|
||||
{ _t('Go back') }
|
||||
</a>;
|
||||
|
@ -702,48 +582,16 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
{ regDoneText }
|
||||
</div>;
|
||||
} else {
|
||||
let yourMatrixAccountText: ReactNode = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
|
||||
// wire up the server details edit link.
|
||||
let editLink = null;
|
||||
if (PHASES_ENABLED &&
|
||||
!SdkConfig.get()['disable_custom_urls'] &&
|
||||
this.state.serverType !== ServerType.FREE &&
|
||||
!this.state.doingUIAuth
|
||||
) {
|
||||
editLink = (
|
||||
<a className="mx_AuthBody_editServerDetails" href="#" onClick={this.onEditServerDetailsClick}>
|
||||
{_t('Change')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
body = <div>
|
||||
<h2>{ _t('Create your account') }</h2>
|
||||
<h2>{ _t('Create account') }</h2>
|
||||
{ errorText }
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.state.phase !== Phase.ServerDetails && <h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3> }
|
||||
<ServerPicker
|
||||
title={_t("Host account on")}
|
||||
dialogTitle={_t("Decide where your account is hosted")}
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.state.doingUIAuth ? undefined : this.props.onServerConfigChange}
|
||||
/>
|
||||
{ this.renderRegisterComponent() }
|
||||
{ goBack }
|
||||
{ signIn }
|
||||
|
|
|
@ -24,8 +24,8 @@ import Modal from '../../../Modal';
|
|||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {sendLoginRequest} from "../../../Login";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
|
@ -101,10 +101,11 @@ export default class SoftLogout extends React.Component {
|
|||
// Note: we don't use the existing Login class because it is heavily flow-based. We don't
|
||||
// care about login flows here, unless it is the single flow we support.
|
||||
const client = MatrixClientPeg.get();
|
||||
const loginViews = (await client.loginFlows()).flows.map(f => FLOWS_TO_VIEWS[f.type]);
|
||||
const flows = (await client.loginFlows()).flows;
|
||||
const loginViews = flows.map(f => FLOWS_TO_VIEWS[f.type]);
|
||||
|
||||
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
|
||||
this.setState({loginView: chosenView});
|
||||
this.setState({ flows, loginView: chosenView });
|
||||
}
|
||||
|
||||
onPasswordChange = (ev) => {
|
||||
|
@ -240,13 +241,18 @@ export default class SoftLogout extends React.Component {
|
|||
introText = _t("Sign in and regain access to your account.");
|
||||
} // else we already have a message and should use it (key backup warning)
|
||||
|
||||
const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso";
|
||||
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{introText}</p>
|
||||
<SSOButton
|
||||
<SSOButtons
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"}
|
||||
flow={flow}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default class CustomServerDialog extends React.Component {
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
{ _t("Custom Server Options") }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t(
|
||||
"You can use the custom server options to sign into other " +
|
||||
"Matrix servers by specifying a different homeserver URL. This " +
|
||||
"allows you to use %(brand)s with an existing Matrix account on a " +
|
||||
"different homeserver.",
|
||||
{ brand },
|
||||
)}</p>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||
{ _t("Dismiss") }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
|
@ -500,17 +499,11 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
});
|
||||
|
||||
try {
|
||||
const requiresIdServerParam =
|
||||
await this.props.matrixClient.doesServerRequireIdServerParam();
|
||||
let result;
|
||||
if (this._submitUrl) {
|
||||
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else if (requiresIdServerParam) {
|
||||
result = await this.props.matrixClient.submitMsisdnToken(
|
||||
this._sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else {
|
||||
throw new Error("The registration with MSISDN flow is misconfigured");
|
||||
}
|
||||
|
@ -519,12 +512,6 @@ export class MsisdnAuthEntry extends React.Component {
|
|||
sid: this._sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
};
|
||||
if (requiresIdServerParam) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.props.matrixClient.getIdentityServerUrl(),
|
||||
);
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
}
|
||||
this.props.submitAuthDict({
|
||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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 * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import ServerConfig from "./ServerConfig";
|
||||
|
||||
const MODULAR_URL = 'https://element.io/matrix-services' +
|
||||
'?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
|
||||
|
||||
// TODO: TravisR - Can this extend ServerConfig for most things?
|
||||
|
||||
/*
|
||||
* Configure the Modular server name.
|
||||
*
|
||||
* This is a variant of ServerConfig with only the HS field and different body
|
||||
* text that is specific to the Modular case.
|
||||
*/
|
||||
export default class ModularServerConfig extends ServerConfig {
|
||||
static propTypes = ServerConfig.propTypes;
|
||||
|
||||
async validateAndApplyServer(hsUrl, isUrl) {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hsUrl,
|
||||
isUrl,
|
||||
busy: true,
|
||||
errorText: "",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async validateServer() {
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const submitButton = this.props.submitText
|
||||
? <AccessibleButton
|
||||
element="button"
|
||||
type="submit"
|
||||
className={this.props.submitClass}
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx_ServerConfig">
|
||||
<h3>{_t("Your server")}</h3>
|
||||
{_t(
|
||||
"Enter the location of your Element Matrix Services homeserver. It may use your own " +
|
||||
"domain name or be a subdomain of <a>element.io</a>.",
|
||||
{}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
)}
|
||||
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field
|
||||
id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
/>
|
||||
</div>
|
||||
{submitButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016, 2017, 2019 New Vector Ltd.
|
||||
Copyright 2015, 2016, 2017, 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -26,7 +26,6 @@ import withValidation from "../elements/Validation";
|
|||
import * as Email from "../../../email";
|
||||
import Field from "../elements/Field";
|
||||
import CountryDropdown from "./CountryDropdown";
|
||||
import SignInToText from "./SignInToText";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
@ -47,7 +46,6 @@ interface IProps {
|
|||
onUsernameBlur?(username: string): void;
|
||||
onPhoneCountryChanged?(phoneCountry: string): void;
|
||||
onPhoneNumberChanged?(phoneNumber: string): void;
|
||||
onEditServerDetailsClick?(): void;
|
||||
onForgotPasswordClick?(): void;
|
||||
}
|
||||
|
||||
|
@ -70,7 +68,6 @@ enum LoginField {
|
|||
*/
|
||||
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onEditServerDetailsClick: null,
|
||||
onUsernameChanged: function() {},
|
||||
onUsernameBlur: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
|
@ -296,7 +293,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
}, {
|
||||
key: "number",
|
||||
test: ({ value }) => !value || PHONE_NUMBER_REGEX.test(value),
|
||||
invalid: () => _t("Doesn't look like a valid phone number"),
|
||||
invalid: () => _t("That phone number doesn't look quite right, please check and try again"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -357,6 +354,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
key="username_input"
|
||||
type="text"
|
||||
label={_t("Username")}
|
||||
placeholder={_t("Username").toLocaleLowerCase()}
|
||||
value={this.props.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onFocus={this.onUsernameFocus}
|
||||
|
@ -410,20 +408,14 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
let forgotPasswordJsx;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = <span>
|
||||
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
|
||||
a: sub => (
|
||||
<AccessibleButton
|
||||
className="mx_Login_forgot"
|
||||
disabled={this.props.busy}
|
||||
kind="link"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
</span>;
|
||||
forgotPasswordJsx = <AccessibleButton
|
||||
className="mx_Login_forgot"
|
||||
disabled={this.props.busy}
|
||||
kind="link"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
>
|
||||
{_t("Forgot password?")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const pwFieldClass = classNames({
|
||||
|
@ -465,8 +457,6 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<SignInToText serverConfig={this.props.serverConfig}
|
||||
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{loginType}
|
||||
{loginField}
|
||||
|
|
|
@ -28,6 +28,8 @@ import withValidation from '../elements/Validation';
|
|||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import PassphraseField from "./PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import Field from '../elements/Field';
|
||||
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
|
||||
|
||||
enum RegistrationField {
|
||||
Email = "field_email",
|
||||
|
@ -51,7 +53,6 @@ interface IProps {
|
|||
}[];
|
||||
serverConfig: ValidatedServerConfig;
|
||||
canSubmit?: boolean;
|
||||
serverRequiresIdServer?: boolean;
|
||||
|
||||
onRegisterClick(params: {
|
||||
username: string;
|
||||
|
@ -104,6 +105,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
|
||||
private onSubmit = async ev => {
|
||||
ev.preventDefault();
|
||||
ev.persist();
|
||||
|
||||
if (!this.props.canSubmit) return;
|
||||
|
||||
|
@ -114,38 +116,24 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
}
|
||||
|
||||
if (this.state.email === '') {
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
|
||||
let desc;
|
||||
if (this.props.serverRequiresIdServer && !haveIs) {
|
||||
desc = _t(
|
||||
"No identity server is configured so you cannot add an email address in order to " +
|
||||
"reset your password in the future.",
|
||||
);
|
||||
} else if (this.showEmail()) {
|
||||
desc = _t(
|
||||
"If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?",
|
||||
);
|
||||
if (this.showEmail()) {
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_warn");
|
||||
Modal.createTrackedDialog("Email prompt dialog", '', RegistrationEmailPromptDialog, {
|
||||
onFinished: async (confirmed: boolean, email?: string) => {
|
||||
if (confirmed) {
|
||||
this.setState({
|
||||
email,
|
||||
}, () => {
|
||||
this.doSubmit(ev);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// user can't set an e-mail so don't prompt them to
|
||||
this.doSubmit(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_warn");
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description: desc,
|
||||
button: _t("Continue"),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.doSubmit(ev);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.doSubmit(ev);
|
||||
}
|
||||
|
@ -357,7 +345,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || phoneNumberLooksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid phone number"),
|
||||
invalid: () => _t("That phone number doesn't look quite right, please check and try again"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -416,11 +404,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
}
|
||||
|
||||
private showEmail() {
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
if (
|
||||
(this.props.serverRequiresIdServer && !haveIs) ||
|
||||
!this.authStepIsUsed('m.login.email.identity')
|
||||
) {
|
||||
if (!this.authStepIsUsed('m.login.email.identity')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -428,12 +412,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
|
||||
private showPhoneNumber() {
|
||||
const threePidLogin = !SdkConfig.get().disable_3pid_login;
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
if (
|
||||
!threePidLogin ||
|
||||
(this.props.serverRequiresIdServer && !haveIs) ||
|
||||
!this.authStepIsUsed('m.login.msisdn')
|
||||
) {
|
||||
if (!threePidLogin || !this.authStepIsUsed('m.login.msisdn')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -443,7 +422,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
if (!this.showEmail()) {
|
||||
return null;
|
||||
}
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ?
|
||||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
|
@ -473,7 +451,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
}
|
||||
|
||||
renderPasswordConfirm() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
id="mx_RegistrationForm_passwordConfirm"
|
||||
ref={field => this[RegistrationField.PasswordConfirm] = field}
|
||||
|
@ -493,7 +470,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
return null;
|
||||
}
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const phoneLabel = this.authStepIsRequired('m.login.msisdn') ?
|
||||
_t("Phone") :
|
||||
_t("Phone (optional)");
|
||||
|
@ -515,13 +491,13 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
}
|
||||
|
||||
renderUsername() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
id="mx_RegistrationForm_username"
|
||||
ref={field => this[RegistrationField.Username] = field}
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
label={_t("Username")}
|
||||
placeholder={_t("Username").toLocaleLowerCase()}
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChange}
|
||||
onValidate={this.onUsernameValidate}
|
||||
|
@ -539,30 +515,22 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
if (this.showEmail()) {
|
||||
if (this.showPhoneNumber()) {
|
||||
emailHelperText = <div>
|
||||
{_t(
|
||||
"Set an email for account recovery. " +
|
||||
"Use email or phone to optionally be discoverable by existing contacts.",
|
||||
)}
|
||||
{
|
||||
_t("Add an email to be able to reset your password.")
|
||||
} {
|
||||
_t("Use email or phone to optionally be discoverable by existing contacts.")
|
||||
}
|
||||
</div>;
|
||||
} else {
|
||||
emailHelperText = <div>
|
||||
{_t(
|
||||
"Set an email for account recovery. " +
|
||||
"Use email to optionally be discoverable by existing contacts.",
|
||||
)}
|
||||
{
|
||||
_t("Add an email to be able to reset your password.")
|
||||
} {
|
||||
_t("Use email to optionally be discoverable by existing contacts.")
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
let noIsText = null;
|
||||
if (this.props.serverRequiresIdServer && !haveIs) {
|
||||
noIsText = <div>
|
||||
{_t(
|
||||
"No identity server is configured so you cannot add an email address in order to " +
|
||||
"reset your password in the future.",
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -579,7 +547,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
{this.renderPhoneNumber()}
|
||||
</div>
|
||||
{ emailHelperText }
|
||||
{ noIsText }
|
||||
{ registerButton }
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,291 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
import classNames from 'classnames';
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
/*
|
||||
* A pure UI component which displays the HS and IS to use.
|
||||
*/
|
||||
|
||||
export default class ServerConfig extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
// The current configuration that the user is expecting to change.
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
|
||||
// Called after the component calls onServerConfigChange
|
||||
onAfterSubmit: PropTypes.func,
|
||||
|
||||
// Optional text for the submit button. If falsey, no button will be shown.
|
||||
submitText: PropTypes.string,
|
||||
|
||||
// Optional class for the submit button. Only applies if the submit button
|
||||
// is to be rendered.
|
||||
submitClass: PropTypes.string,
|
||||
|
||||
// Whether the flow this component is embedded in requires an identity
|
||||
// server when the homeserver says it will need one. Default false.
|
||||
showIdentityServerIfRequiredByHomeserver: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onServerConfigChange: function() {},
|
||||
delayTimeMs: 0,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
busy: false,
|
||||
errorText: "",
|
||||
hsUrl: props.serverConfig.hsUrl,
|
||||
isUrl: props.serverConfig.isUrl,
|
||||
showIdentityServer: false,
|
||||
};
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_custom_server");
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.state.isUrl) return;
|
||||
|
||||
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
||||
}
|
||||
|
||||
async validateServer() {
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
const result = this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// If the UI flow this component is embedded in requires an identity
|
||||
// server when the homeserver says it will need one, check first and
|
||||
// reveal this field if not already shown.
|
||||
// XXX: This a backward compatibility path for homeservers that require
|
||||
// an identity server to be passed during certain flows.
|
||||
// See also https://github.com/matrix-org/synapse/pull/5868.
|
||||
if (
|
||||
this.props.showIdentityServerIfRequiredByHomeserver &&
|
||||
!this.state.showIdentityServer &&
|
||||
await this.isIdentityServerRequiredByHomeserver()
|
||||
) {
|
||||
this.setState({
|
||||
showIdentityServer: true,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async validateAndApplyServer(hsUrl, isUrl) {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
|
||||
this.setState({
|
||||
hsUrl: defaultConfig.hsUrl,
|
||||
isUrl: defaultConfig.isUrl,
|
||||
busy: false,
|
||||
errorText: "",
|
||||
});
|
||||
this.props.onServerConfigChange(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hsUrl,
|
||||
isUrl,
|
||||
busy: true,
|
||||
errorText: "",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (!stateForError.isFatalError) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
// carry on anyway
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} else {
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async isIdentityServerRequiredByHomeserver() {
|
||||
// XXX: We shouldn't have to create a whole new MatrixClient just to
|
||||
// check if the homeserver requires an identity server... Should it be
|
||||
// extracted to a static utils function...?
|
||||
return createClient({
|
||||
baseUrl: this.state.hsUrl,
|
||||
}).doesServerRequireIdServerParam();
|
||||
}
|
||||
|
||||
onHomeserverBlur = (ev) => {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||
this.validateServer();
|
||||
});
|
||||
};
|
||||
|
||||
onHomeserverChange = (ev) => {
|
||||
const hsUrl = ev.target.value;
|
||||
this.setState({ hsUrl });
|
||||
};
|
||||
|
||||
onIdentityServerBlur = (ev) => {
|
||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
|
||||
this.validateServer();
|
||||
});
|
||||
};
|
||||
|
||||
onIdentityServerChange = (ev) => {
|
||||
const isUrl = ev.target.value;
|
||||
this.setState({ isUrl });
|
||||
};
|
||||
|
||||
onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const result = await this.validateServer();
|
||||
if (!result) return; // Do not continue.
|
||||
|
||||
if (this.props.onAfterSubmit) {
|
||||
this.props.onAfterSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
_waitThenInvoke(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
clearTimeout(existingTimeoutId);
|
||||
}
|
||||
return setTimeout(fn.bind(this), this.props.delayTimeMs);
|
||||
}
|
||||
|
||||
showHelpPopup = () => {
|
||||
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
|
||||
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||
};
|
||||
|
||||
_renderHomeserverSection() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <div>
|
||||
{_t("Enter your custom homeserver URL <a>What does this mean?</a>", {}, {
|
||||
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field
|
||||
id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderIdentityServerSection() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const classes = classNames({
|
||||
"mx_ServerConfig_identityServer": true,
|
||||
"mx_ServerConfig_identityServer_shown": this.state.showIdentityServer,
|
||||
});
|
||||
return <div className={classes}>
|
||||
{_t("Enter your custom identity server URL <a>What does this mean?</a>", {}, {
|
||||
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.serverConfig.isUrl}
|
||||
value={this.state.isUrl || ''}
|
||||
onBlur={this.onIdentityServerBlur}
|
||||
onChange={this.onIdentityServerChange}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const errorText = this.state.errorText
|
||||
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
|
||||
: null;
|
||||
|
||||
const submitButton = this.props.submitText
|
||||
? <AccessibleButton
|
||||
element="button"
|
||||
type="submit"
|
||||
className={this.props.submitClass}
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<form className="mx_ServerConfig" onSubmit={this.onSubmit} autoComplete="off">
|
||||
<h3>{_t("Other servers")}</h3>
|
||||
{errorText}
|
||||
{this._renderHomeserverSection()}
|
||||
{this._renderIdentityServerSection()}
|
||||
{submitButton}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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 * as sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import {makeType} from "../../../utils/TypeUtils";
|
||||
|
||||
const MODULAR_URL = 'https://element.io/matrix-services' +
|
||||
'?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
|
||||
|
||||
export const FREE = 'Free';
|
||||
export const PREMIUM = 'Premium';
|
||||
export const ADVANCED = 'Advanced';
|
||||
|
||||
export const TYPES = {
|
||||
FREE: {
|
||||
id: FREE,
|
||||
label: () => _t('Free'),
|
||||
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
|
||||
description: () => _t('Join millions for free on the largest public server'),
|
||||
serverConfig: makeType(ValidatedServerConfig, {
|
||||
hsUrl: "https://matrix-client.matrix.org",
|
||||
hsName: "matrix.org",
|
||||
hsNameIsDifferent: false,
|
||||
isUrl: "https://vector.im",
|
||||
}),
|
||||
},
|
||||
PREMIUM: {
|
||||
id: PREMIUM,
|
||||
label: () => _t('Premium'),
|
||||
logo: () => <img src={require('../../../../res/img/ems-logo.svg')} height={16} />,
|
||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
}),
|
||||
identityServerUrl: "https://vector.im",
|
||||
},
|
||||
ADVANCED: {
|
||||
id: ADVANCED,
|
||||
label: () => _t('Advanced'),
|
||||
logo: () => <div>
|
||||
<img src={require('../../../../res/img/feather-customised/globe.svg')} />
|
||||
{_t('Other')}
|
||||
</div>,
|
||||
description: () => _t('Find other public servers or use a custom server'),
|
||||
},
|
||||
};
|
||||
|
||||
export function getTypeFromServerConfig(config) {
|
||||
const {hsUrl} = config;
|
||||
if (!hsUrl) {
|
||||
return null;
|
||||
} else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
|
||||
return FREE;
|
||||
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
|
||||
// This is an unlikely case to reach, as Modular defaults to hiding the
|
||||
// server type selector.
|
||||
return PREMIUM;
|
||||
} else {
|
||||
return ADVANCED;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ServerTypeSelector extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The default selected type.
|
||||
selected: PropTypes.string,
|
||||
// Handler called when the selected type changes.
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
selected,
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
selected,
|
||||
};
|
||||
}
|
||||
|
||||
updateSelectedType(type) {
|
||||
if (this.state.selected === type) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selected: type,
|
||||
});
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(type);
|
||||
}
|
||||
}
|
||||
|
||||
onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
const type = e.currentTarget.dataset.id;
|
||||
this.updateSelectedType(type);
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const serverTypes = [];
|
||||
for (const type of Object.values(TYPES)) {
|
||||
const { id, label, logo, description } = type;
|
||||
const classes = classnames(
|
||||
"mx_ServerTypeSelector_type",
|
||||
`mx_ServerTypeSelector_type_${id}`,
|
||||
{
|
||||
"mx_ServerTypeSelector_type_selected": id === this.state.selected,
|
||||
},
|
||||
);
|
||||
|
||||
serverTypes.push(<div className={classes} key={id} >
|
||||
<div className="mx_ServerTypeSelector_label">
|
||||
{label()}
|
||||
</div>
|
||||
<AccessibleButton onClick={this.onClick} data-id={id}>
|
||||
<div className="mx_ServerTypeSelector_logo">
|
||||
{logo()}
|
||||
</div>
|
||||
<div className="mx_ServerTypeSelector_description">
|
||||
{description()}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return <div className="mx_ServerTypeSelector">
|
||||
{serverTypes}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {_t} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import PropTypes from "prop-types";
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
export default class SignInToText extends React.PureComponent {
|
||||
static propTypes = {
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
if (this.props.onEditServerDetailsClick) {
|
||||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
return <h3>
|
||||
{signInToText}
|
||||
{editLink}
|
||||
</h3>;
|
||||
}
|
||||
}
|
59
src/components/views/context_menus/CallContextMenu.tsx
Normal file
59
src/components/views/context_menus/CallContextMenu.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
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 { ContextMenu, IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
call: MatrixCall;
|
||||
}
|
||||
|
||||
export default class CallContextMenu extends React.Component<IProps> {
|
||||
static propTypes = {
|
||||
// js-sdk User object. Not required because it might not exist.
|
||||
user: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onHoldClick = () => {
|
||||
this.props.call.setRemoteOnHold(true);
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
onUnholdClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
|
||||
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
render() {
|
||||
const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold");
|
||||
const handler = this.props.call.isRemoteOnHold() ? this.onUnholdClick : this.onHoldClick;
|
||||
|
||||
return <ContextMenu {...this.props}>
|
||||
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
||||
{holdUnholdCaption}
|
||||
</MenuItem>
|
||||
</ContextMenu>;
|
||||
}
|
||||
}
|
|
@ -149,7 +149,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
onRedactClick = () => {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: async (proceed) => {
|
||||
onFinished: async (proceed, reason) => {
|
||||
if (!proceed) return;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -157,6 +157,8 @@ export default class MessageContextMenu extends React.Component {
|
|||
await cli.redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
undefined,
|
||||
reason ? { reason } : {},
|
||||
);
|
||||
} catch (e) {
|
||||
const code = e.errcode || e.statusCode;
|
||||
|
|
|
@ -57,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
let unpinButton;
|
||||
if (showUnpin) {
|
||||
const onUnpinClick = () => {
|
||||
WidgetStore.instance.unpinWidget(app.id);
|
||||
WidgetStore.instance.unpinWidget(room.roomId, app.id);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
@ -143,7 +143,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
let moveLeftButton;
|
||||
if (showUnpin && widgetIndex > 0) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(app.id, -1);
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, -1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
@ -153,7 +153,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
let moveRightButton;
|
||||
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(app.id, 1);
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, 1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
|
|
@ -23,15 +23,17 @@ import { _t } from '../../../languageHandler';
|
|||
*/
|
||||
export default class ConfirmRedactDialog extends React.Component {
|
||||
render() {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
|
||||
return (
|
||||
<QuestionDialog onFinished={this.props.onFinished}
|
||||
<TextInputDialog onFinished={this.props.onFinished}
|
||||
title={_t("Confirm Removal")}
|
||||
description={
|
||||
_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
placeholder={_t("Reason (optional)")}
|
||||
focus
|
||||
button={_t("Remove")}>
|
||||
</QuestionDialog>
|
||||
</TextInputDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export default class InfoDialog extends React.Component {
|
|||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
onKeyDown: PropTypes.func,
|
||||
fixedWidth: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -54,6 +55,7 @@ export default class InfoDialog extends React.Component {
|
|||
contentId='mx_Dialog_content'
|
||||
hasCancel={this.props.hasCloseButton}
|
||||
onKeyDown={this.props.onKeyDown}
|
||||
fixedWidth={this.props.fixedWidth}
|
||||
>
|
||||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
|
|
|
@ -15,13 +15,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/src/matrix";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import * as Email from "../../../email";
|
||||
|
@ -132,12 +131,12 @@ class ThreepidMember extends Member {
|
|||
}
|
||||
}
|
||||
|
||||
class DMUserTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
onRemove: PropTypes.func, // takes 1 argument, the member being removed
|
||||
};
|
||||
interface IDMUserTileProps {
|
||||
member: RoomMember;
|
||||
onRemove: (RoomMember) => any;
|
||||
}
|
||||
|
||||
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||
_onRemove = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
|
@ -173,7 +172,9 @@ class DMUserTile extends React.PureComponent {
|
|||
className='mx_InviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
||||
alt={_t('Remove')} width={8} height={8}
|
||||
/>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -190,15 +191,15 @@ class DMUserTile extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class DMRoomTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
lastActiveTs: PropTypes.number,
|
||||
onToggle: PropTypes.func.isRequired, // takes 1 argument, the member being toggled
|
||||
highlightWord: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
};
|
||||
interface IDMRoomTileProps {
|
||||
member: RoomMember;
|
||||
lastActiveTs: number;
|
||||
onToggle: (RoomMember) => any;
|
||||
highlightWord: string;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||
_onClick = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
|
@ -298,28 +299,45 @@ class DMRoomTile extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
export default class InviteDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// Takes an array of user IDs/emails to invite.
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
interface IInviteDialogProps {
|
||||
// Takes an array of user IDs/emails to invite.
|
||||
onFinished: (toInvite?: string[]) => any;
|
||||
|
||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||
// not provided.
|
||||
kind: PropTypes.string,
|
||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||
// not provided.
|
||||
kind: string,
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: PropTypes.string,
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: string,
|
||||
|
||||
// Initial value to populate the filter with
|
||||
initialText: PropTypes.string,
|
||||
};
|
||||
// Initial value to populate the filter with
|
||||
initialText: string,
|
||||
}
|
||||
|
||||
interface IInviteDialogState {
|
||||
targets: RoomMember[]; // array of Member objects (see interface above)
|
||||
filterText: string;
|
||||
recents: { user: Member, userId: string }[];
|
||||
numRecentsShown: number;
|
||||
suggestions: { user: Member, userId: string }[];
|
||||
numSuggestionsShown: number;
|
||||
serverResultsMixin: { user: Member, userId: string }[];
|
||||
threepidResultsMixin: { user: Member, userId: string}[];
|
||||
canUseIdentityServer: boolean;
|
||||
tryingIdentityServer: boolean;
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: boolean,
|
||||
errorText: string,
|
||||
}
|
||||
|
||||
export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
|
||||
static defaultProps = {
|
||||
kind: KIND_DM,
|
||||
initialText: "",
|
||||
};
|
||||
|
||||
_debounceTimer: number = null;
|
||||
_debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||
_editorRef: any = null;
|
||||
|
||||
constructor(props) {
|
||||
|
@ -348,8 +366,8 @@ export default class InviteDialog extends React.PureComponent {
|
|||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||
suggestions: this._buildSuggestions(alreadyInvited),
|
||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
|
||||
threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
|
||||
serverResultsMixin: [],
|
||||
threepidResultsMixin: [],
|
||||
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
tryingIdentityServer: false,
|
||||
|
||||
|
@ -367,7 +385,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
||||
|
@ -430,7 +448,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
return recents;
|
||||
}
|
||||
|
||||
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember} {
|
||||
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
||||
const maxConsideredMembers = 200;
|
||||
const joinedRooms = MatrixClientPeg.get().getRooms()
|
||||
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
||||
|
@ -470,7 +488,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
}, {});
|
||||
|
||||
// Generates { userId: {member, numRooms, score} }
|
||||
const memberScores = Object.values(memberRooms).reduce((scores, entry) => {
|
||||
const memberScores = Object.values(memberRooms).reduce((scores, entry: {member: RoomMember, rooms: Room[]}) => {
|
||||
const numMembersTotal = entry.rooms.reduce((c, r) => c + r.getJoinedMemberCount(), 0);
|
||||
const maxRange = maxConsideredMembers * entry.rooms.length;
|
||||
scores[entry.member.userId] = {
|
||||
|
@ -603,7 +621,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const createRoomOptions = {inlineErrors: true};
|
||||
const createRoomOptions = {inlineErrors: true} as any; // XXX: Type out `createRoomOptions`
|
||||
|
||||
if (privateShouldBeEncrypted()) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
|
@ -620,7 +638,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
// Check if it's a traditional DM and create the room if required.
|
||||
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||
let createRoomPromise = Promise.resolve();
|
||||
let createRoomPromise = Promise.resolve(null) as Promise<string | null | boolean>;
|
||||
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
|
||||
if (targetIds.length === 1 && !isSelf) {
|
||||
createRoomOptions.dmUserId = targetIds[0];
|
||||
|
@ -990,7 +1008,8 @@ export default class InviteDialog extends React.PureComponent {
|
|||
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
|
||||
if (this.state.filterText && hasMixins && kind === 'suggestions') {
|
||||
// We don't want to duplicate members though, so just exclude anyone we've already seen.
|
||||
const notAlreadyExists = (u: Member): boolean => {
|
||||
// The type of u is a pain to define but members of both mixins have the 'userId' property
|
||||
const notAlreadyExists = (u: any): boolean => {
|
||||
return !sourceMembers.some(m => m.userId === u.userId)
|
||||
&& !priorityAdditionalMembers.some(m => m.userId === u.userId)
|
||||
&& !otherAdditionalMembers.some(m => m.userId === u.userId);
|
||||
|
@ -1169,7 +1188,8 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
const inviteText = _t("This won't invite them to %(communityName)s. " +
|
||||
const inviteText = _t(
|
||||
"This won't invite them to %(communityName)s. " +
|
||||
"To invite someone to %(communityName)s, click <a>here</a>",
|
||||
{communityName}, {
|
||||
userId: () => {
|
||||
|
@ -1209,7 +1229,9 @@ export default class InviteDialog extends React.PureComponent {
|
|||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
|
@ -1220,7 +1242,9 @@ export default class InviteDialog extends React.PureComponent {
|
|||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 * as React from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import {useRef, useState} from "react";
|
||||
import Field from "../elements/Field";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import withValidation from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
onFinished(continued: boolean, email?: string): void;
|
||||
}
|
||||
|
||||
const validation = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const fieldRef = useRef<Field>();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (email) {
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!valid) {
|
||||
fieldRef.current.focus();
|
||||
fieldRef.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onFinished(true, email);
|
||||
};
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Continuing without email")}
|
||||
className="mx_RegistrationEmailPromptDialog"
|
||||
contentId="mx_RegistrationEmailPromptDialog"
|
||||
onFinished={() => onFinished(false)}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_Dialog_content" id="mx_RegistrationEmailPromptDialog">
|
||||
<p>{_t("Just a heads up, if you don't add an email and forget your password, you could " +
|
||||
"<b>permanently lose access to your account</b>.", {}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
})}</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Field
|
||||
ref={fieldRef}
|
||||
type="text"
|
||||
label={_t("Email (optional)")}
|
||||
value={email}
|
||||
onChange={ev => {
|
||||
setEmail(ev.target.value);
|
||||
}}
|
||||
onValidate={async fieldState => await validation(fieldState)}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={onSubmit}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default RegistrationEmailPromptDialog;
|
|
@ -53,9 +53,9 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
// When room 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
|
||||
if (payload.action === 'view_next_room') {
|
||||
if (payload.action === 'view_home_page') {
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
|
235
src/components/views/dialogs/ServerPickerDialog.tsx
Normal file
235
src/components/views/dialogs/ServerPickerDialog.tsx
Normal file
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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, {createRef} from "react";
|
||||
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
|
||||
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import BaseDialog from './BaseDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import Field from "../elements/Field";
|
||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import withValidation, {IFieldState} from "../elements/Validation";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
serverConfig: ValidatedServerConfig;
|
||||
onFinished(config?: ValidatedServerConfig): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
defaultChosen: boolean;
|
||||
otherHomeserver: string;
|
||||
}
|
||||
|
||||
export default class ServerPickerDialog extends React.PureComponent<IProps, IState> {
|
||||
private readonly defaultServer: ValidatedServerConfig;
|
||||
private readonly fieldRef = createRef<Field>();
|
||||
private validatedConf: ValidatedServerConfig;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const config = SdkConfig.get();
|
||||
this.defaultServer = config["validated_server_config"] as ValidatedServerConfig;
|
||||
const { serverConfig } = this.props;
|
||||
|
||||
let otherHomeserver = "";
|
||||
if (!serverConfig.isDefault) {
|
||||
if (serverConfig.isNameResolvable && serverConfig.hsName) {
|
||||
otherHomeserver = serverConfig.hsName;
|
||||
} else {
|
||||
otherHomeserver = serverConfig.hsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
defaultChosen: serverConfig.isDefault,
|
||||
otherHomeserver,
|
||||
};
|
||||
}
|
||||
|
||||
private onDefaultChosen = () => {
|
||||
this.setState({ defaultChosen: true });
|
||||
};
|
||||
|
||||
private onOtherChosen = () => {
|
||||
this.setState({ defaultChosen: false });
|
||||
};
|
||||
|
||||
private onHomeserverChange = (ev) => {
|
||||
this.setState({ otherHomeserver: ev.target.value });
|
||||
};
|
||||
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
private validate = withValidation<this, { error?: string }>({
|
||||
deriveData: async ({ value }) => {
|
||||
let hsUrl = value.trim(); // trim to account for random whitespace
|
||||
|
||||
// if the URL has no protocol, try validate it as a serverName via well-known
|
||||
if (!hsUrl.includes("://")) {
|
||||
try {
|
||||
const discoveryResult = await AutoDiscovery.findClientConfig(hsUrl);
|
||||
this.validatedConf = AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(hsUrl, discoveryResult);
|
||||
return {}; // we have a validated config, we don't need to try the other paths
|
||||
} catch (e) {
|
||||
console.error(`Attempted ${hsUrl} as a server_name but it failed`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// if we got to this stage then either the well-known failed or the URL had a protocol specified,
|
||||
// so validate statically only. If the URL has no protocol, default to https.
|
||||
if (!hsUrl.includes("://")) {
|
||||
hsUrl = "https://" + hsUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl);
|
||||
return {};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (stateForError.isFatalError) {
|
||||
let error = _t("Unable to validate homeserver");
|
||||
if (e.translatedMessage) {
|
||||
error = e.translatedMessage;
|
||||
}
|
||||
return { error };
|
||||
}
|
||||
|
||||
// try to carry on anyway
|
||||
try {
|
||||
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true);
|
||||
return {};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { error: _t("Invalid URL") };
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Specify a homeserver"),
|
||||
}, {
|
||||
key: "valid",
|
||||
test: async function({ value }, { error }) {
|
||||
if (!value) return true;
|
||||
return !error;
|
||||
},
|
||||
invalid: function({ error }) {
|
||||
return error;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onHomeserverValidate = (fieldState: IFieldState) => this.validate(fieldState);
|
||||
|
||||
private onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const valid = await this.fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!valid) {
|
||||
this.fieldRef.current.focus();
|
||||
this.fieldRef.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onFinished(this.validatedConf);
|
||||
};
|
||||
|
||||
public render() {
|
||||
let text;
|
||||
if (this.defaultServer.hsName === "matrix.org") {
|
||||
text = _t("Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.");
|
||||
}
|
||||
|
||||
let defaultServerName = this.defaultServer.hsName;
|
||||
if (this.defaultServer.hsNameIsDifferent) {
|
||||
defaultServerName = (
|
||||
<TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>
|
||||
{this.defaultServer.hsName}
|
||||
</TextWithTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={this.props.title || _t("Sign into your homeserver")}
|
||||
className="mx_ServerPickerDialog"
|
||||
contentId="mx_ServerPickerDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
fixedWidth={false}
|
||||
hasCancel={true}
|
||||
>
|
||||
<form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
|
||||
<p>
|
||||
{_t("We call the places where you can host your account ‘homeservers’.")} {text}
|
||||
</p>
|
||||
|
||||
<StyledRadioButton
|
||||
name="defaultChosen"
|
||||
value="true"
|
||||
checked={this.state.defaultChosen}
|
||||
onChange={this.onDefaultChosen}
|
||||
>
|
||||
{defaultServerName}
|
||||
</StyledRadioButton>
|
||||
|
||||
<StyledRadioButton
|
||||
name="defaultChosen"
|
||||
value="false"
|
||||
className="mx_ServerPickerDialog_otherHomeserverRadio"
|
||||
checked={!this.state.defaultChosen}
|
||||
onChange={this.onOtherChosen}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
className="mx_ServerPickerDialog_otherHomeserver"
|
||||
label={_t("Other homeserver")}
|
||||
onChange={this.onHomeserverChange}
|
||||
onClick={this.onOtherChosen}
|
||||
ref={this.fieldRef}
|
||||
onValidate={this.onHomeserverValidate}
|
||||
value={this.state.otherHomeserver}
|
||||
validateOnChange={false}
|
||||
validateOnFocus={false}
|
||||
/>
|
||||
</StyledRadioButton>
|
||||
<p>
|
||||
{_t("Use your preferred Matrix homeserver if you have one, or host your own.")}
|
||||
</p>
|
||||
|
||||
<AccessibleButton className="mx_ServerPickerDialog_continue" kind="primary" onClick={this.onSubmit}>
|
||||
{_t("Continue")}
|
||||
</AccessibleButton>
|
||||
|
||||
<h4>{_t("Learn more")}</h4>
|
||||
<a href="https://matrix.org/faq/#what-is-a-homeserver%3F" target="_blank" rel="noreferrer noopener">
|
||||
{_t("About homeservers")}
|
||||
</a>
|
||||
</form>
|
||||
</BaseDialog>;
|
||||
}
|
||||
}
|
|
@ -146,7 +146,7 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
const events = this.props.target.getLiveTimeline().getEvents();
|
||||
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = this.state.permalinkCreator.forRoom();
|
||||
matrixToUrl = this.state.permalinkCreator.forShareableRoom();
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
|
|
|
@ -375,17 +375,20 @@ export default class AppTile extends React.Component {
|
|||
</div>
|
||||
);
|
||||
|
||||
// all widgets can theoretically be allowed to remain on screen, so we wrap
|
||||
// them all in a PersistedElement from the get-go. If we wait, the iframe will
|
||||
// be re-mounted later, which means the widget has to start over, which is bad.
|
||||
if (!this.props.userWidget) {
|
||||
// All room widgets can theoretically be allowed to remain on screen, so we
|
||||
// wrap them all in a PersistedElement from the get-go. If we wait, the iframe
|
||||
// will be re-mounted later, which means the widget has to start over, which is
|
||||
// bad.
|
||||
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
94
src/components/views/elements/EffectsOverlay.tsx
Normal file
94
src/components/views/elements/EffectsOverlay.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Copyright 2020 Nurjin Jafar
|
||||
Copyright 2020 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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, { FunctionComponent, useEffect, useRef } from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ICanvasEffect from '../../../effects/ICanvasEffect';
|
||||
import {CHAT_EFFECTS} from '../../../effects'
|
||||
|
||||
interface IProps {
|
||||
roomWidth: number;
|
||||
}
|
||||
|
||||
const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const effectsRef = useRef<Map<string, ICanvasEffect>>(new Map<string, ICanvasEffect>());
|
||||
|
||||
const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
|
||||
if (!name) return null;
|
||||
let effect: ICanvasEffect | null = effectsRef.current[name] || null;
|
||||
if (effect === null) {
|
||||
const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
|
||||
try {
|
||||
const { default: Effect } = await import(`../../../effects/${name}`);
|
||||
effect = new Effect(options);
|
||||
effectsRef.current[name] = effect;
|
||||
} catch (err) {
|
||||
console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err);
|
||||
}
|
||||
}
|
||||
return effect;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const resize = () => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.height = window.innerHeight;
|
||||
}
|
||||
};
|
||||
const onAction = (payload: { action: string }) => {
|
||||
const actionPrefix = 'effects.';
|
||||
if (payload.action.indexOf(actionPrefix) === 0) {
|
||||
const effect = payload.action.substr(actionPrefix.length);
|
||||
lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
|
||||
}
|
||||
}
|
||||
const dispatcherRef = dis.register(onAction);
|
||||
const canvas = canvasRef.current;
|
||||
canvas.height = window.innerHeight;
|
||||
window.addEventListener('resize', resize, true);
|
||||
|
||||
return () => {
|
||||
dis.unregister(dispatcherRef);
|
||||
window.removeEventListener('resize', resize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
||||
for (const effect in currentEffects) {
|
||||
const effectModule: ICanvasEffect = currentEffects[effect];
|
||||
if (effectModule && effectModule.isRunning) {
|
||||
effectModule.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={roomWidth}
|
||||
style={{
|
||||
display: 'block',
|
||||
zIndex: 999999,
|
||||
pointerEvents: 'none',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default EffectsOverlay;
|
|
@ -61,6 +61,10 @@ interface IProps {
|
|||
tooltipClassName?: string;
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className?: string;
|
||||
// On what events should validation occur; by default on all
|
||||
validateOnFocus?: boolean;
|
||||
validateOnBlur?: boolean;
|
||||
validateOnChange?: boolean;
|
||||
// All other props pass through to the <input>.
|
||||
}
|
||||
|
||||
|
@ -100,6 +104,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
public static readonly defaultProps = {
|
||||
element: "input",
|
||||
type: "text",
|
||||
validateOnFocus: true,
|
||||
validateOnBlur: true,
|
||||
validateOnChange: true,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -137,9 +144,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
this.setState({
|
||||
focused: true,
|
||||
});
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
if (this.props.validateOnFocus) {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}
|
||||
// Parent component may have supplied its own `onFocus` as well
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(ev);
|
||||
|
@ -147,7 +156,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
};
|
||||
|
||||
private onChange = (ev) => {
|
||||
this.validateOnChange();
|
||||
if (this.props.validateOnChange) {
|
||||
this.validateOnChange();
|
||||
}
|
||||
// Parent component may have supplied its own `onChange` as well
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(ev);
|
||||
|
@ -158,16 +169,18 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
this.setState({
|
||||
focused: false,
|
||||
});
|
||||
this.validate({
|
||||
focused: false,
|
||||
});
|
||||
if (this.props.validateOnBlur) {
|
||||
this.validate({
|
||||
focused: false,
|
||||
});
|
||||
}
|
||||
// Parent component may have supplied its own `onBlur` as well
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(ev);
|
||||
}
|
||||
};
|
||||
|
||||
private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
|
||||
public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
|
@ -196,12 +209,15 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
feedbackVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
public render() {
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { element, prefixComponent, postfixComponent, className, onValidate, children,
|
||||
tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props;
|
||||
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
|
||||
...inputProps} = this.props;
|
||||
|
||||
// Set some defaults for the <input> element
|
||||
const ref = input => this.input = input;
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
const SSOButton = ({matrixClient, loginType, fragmentAfterLogin, ...props}) => {
|
||||
const onClick = () => {
|
||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} kind="primary" onClick={onClick}>
|
||||
{_t("Sign in with single sign-on")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
SSOButton.propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
|
||||
loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
|
||||
fragmentAfterLogin: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SSOButton;
|
110
src/components/views/elements/SSOButtons.tsx
Normal file
110
src/components/views/elements/SSOButtons.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||
idp: IIdentityProvider;
|
||||
mini?: boolean;
|
||||
}
|
||||
|
||||
const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
matrixClient,
|
||||
loginType,
|
||||
fragmentAfterLogin,
|
||||
idp,
|
||||
primary,
|
||||
mini,
|
||||
...props
|
||||
}) => {
|
||||
const kind = primary ? "primary" : "primary_outline";
|
||||
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
|
||||
|
||||
const onClick = () => {
|
||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id);
|
||||
};
|
||||
|
||||
let icon;
|
||||
if (idp && idp.icon && idp.icon.startsWith("https://")) {
|
||||
icon = <img src={idp.icon} height="24" width="24" alt={label} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_SSOButton", {
|
||||
mx_SSOButton_mini: mini,
|
||||
});
|
||||
|
||||
if (mini) {
|
||||
// TODO fallback icon
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
{ icon }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
{ icon }
|
||||
{ label }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
flow: ISSOFlow;
|
||||
loginType?: "sso" | "cas";
|
||||
fragmentAfterLogin?: string;
|
||||
primary?: boolean;
|
||||
}
|
||||
|
||||
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
|
||||
const providers = flow["org.matrix.msc2858.identity_providers"] || [];
|
||||
if (providers.length < 2) {
|
||||
return <div className="mx_SSOButtons">
|
||||
<SSOButton
|
||||
matrixClient={matrixClient}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={providers[0]}
|
||||
primary={primary}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_SSOButtons">
|
||||
{ providers.map(idp => (
|
||||
<SSOButton
|
||||
key={idp.id}
|
||||
matrixClient={matrixClient}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={idp}
|
||||
mini={true}
|
||||
primary={primary}
|
||||
/>
|
||||
)) }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SSOButtons;
|
93
src/components/views/elements/ServerPicker.tsx
Normal file
93
src/components/views/elements/ServerPicker.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 AccessibleButton from "./AccessibleButton";
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import TextWithTooltip from "./TextWithTooltip";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import Modal from "../../../Modal";
|
||||
import ServerPickerDialog from "../dialogs/ServerPickerDialog";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
dialogTitle?: string;
|
||||
serverConfig: ValidatedServerConfig;
|
||||
onServerConfigChange?(config: ValidatedServerConfig): void;
|
||||
}
|
||||
|
||||
const showPickerDialog = (
|
||||
title: string,
|
||||
serverConfig: ValidatedServerConfig,
|
||||
onFinished: (config: ValidatedServerConfig) => void,
|
||||
) => {
|
||||
Modal.createTrackedDialog("Server Picker", "", ServerPickerDialog, { title, serverConfig, onFinished });
|
||||
};
|
||||
|
||||
const onHelpClick = () => {
|
||||
Modal.createTrackedDialog('Custom Server Dialog', '', InfoDialog, {
|
||||
title: _t("Server Options"),
|
||||
description: _t("You can use the custom server options to sign into other Matrix servers by specifying " +
|
||||
"a different homeserver URL. This allows you to use Element with an existing Matrix account on " +
|
||||
"a different homeserver."),
|
||||
button: _t("Dismiss"),
|
||||
hasCloseButton: false,
|
||||
fixedWidth: false,
|
||||
}, "mx_ServerPicker_helpDialog");
|
||||
};
|
||||
|
||||
const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }: IProps) => {
|
||||
let editBtn;
|
||||
if (!SdkConfig.get()["disable_custom_urls"] && onServerConfigChange) {
|
||||
const onClick = () => {
|
||||
showPickerDialog(dialogTitle, serverConfig, (config?: ValidatedServerConfig) => {
|
||||
if (config) {
|
||||
onServerConfigChange(config);
|
||||
}
|
||||
});
|
||||
};
|
||||
editBtn = <AccessibleButton className="mx_ServerPicker_change" kind="link" onClick={onClick}>
|
||||
{_t("Edit")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let serverName = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl;
|
||||
if (serverConfig.hsNameIsDifferent) {
|
||||
serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
|
||||
{serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
}
|
||||
|
||||
let desc;
|
||||
if (serverConfig.hsName === "matrix.org") {
|
||||
desc = <span className="mx_ServerPicker_desc">
|
||||
{_t("Join millions for free on the largest public server")}
|
||||
</span>;
|
||||
}
|
||||
|
||||
return <div className="mx_ServerPicker">
|
||||
<h3>{title || _t("Homeserver")}</h3>
|
||||
<AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} />
|
||||
<span className="mx_ServerPicker_server">{serverName}</span>
|
||||
{ editBtn }
|
||||
{ desc }
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ServerPicker;
|
|
@ -35,7 +35,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
|||
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
|
||||
|
||||
let joinCopy = _t('Join the conference at the top of this room');
|
||||
if (!WidgetStore.instance.isPinned(this.props.mxEvent.getStateKey())) {
|
||||
if (!WidgetStore.instance.isPinned(this.props.mxEvent.getRoomId(), this.props.mxEvent.getStateKey())) {
|
||||
joinCopy = _t('Join the conference from the room information card on the right');
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ import RoomContext from "../../../contexts/RoomContext";
|
|||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -83,9 +84,10 @@ export const useWidgets = (room: Room) => {
|
|||
|
||||
interface IAppRowProps {
|
||||
app: IApp;
|
||||
room: Room;
|
||||
}
|
||||
|
||||
const AppRow: React.FC<IAppRowProps> = ({ app }) => {
|
||||
const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
const name = WidgetUtils.getWidgetName(app);
|
||||
const dataTitle = WidgetUtils.getWidgetDataTitle(app);
|
||||
const subtitle = dataTitle && " - " + dataTitle;
|
||||
|
@ -100,10 +102,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const isPinned = WidgetStore.instance.isPinned(app.id);
|
||||
const isPinned = WidgetStore.instance.isPinned(room.roomId, app.id);
|
||||
const togglePin = isPinned
|
||||
? () => { WidgetStore.instance.unpinWidget(app.id); }
|
||||
: () => { WidgetStore.instance.pinWidget(app.id); };
|
||||
? () => { WidgetStore.instance.unpinWidget(room.roomId, app.id); }
|
||||
: () => { WidgetStore.instance.pinWidget(room.roomId, app.id); };
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
let contextMenu;
|
||||
|
@ -118,7 +120,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app }) => {
|
|||
/>;
|
||||
}
|
||||
|
||||
const cannotPin = !isPinned && !WidgetStore.instance.canPin(app.id);
|
||||
const cannotPin = !isPinned && !WidgetStore.instance.canPin(room.roomId, app.id);
|
||||
|
||||
let pinTitle: string;
|
||||
if (cannotPin) {
|
||||
|
@ -183,7 +185,7 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
|||
};
|
||||
|
||||
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||
{ apps.map(app => <AppRow key={app.id} app={app} />) }
|
||||
{ apps.map(app => <AppRow key={app.id} app={app} room={room} />) }
|
||||
|
||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
||||
|
@ -209,14 +211,6 @@ const onRoomSettingsClick = () => {
|
|||
defaultDispatcher.dispatch({ action: "open_room_settings" });
|
||||
};
|
||||
|
||||
const useMemberCount = (room: Room) => {
|
||||
const [count, setCount] = useState(room.getJoinedMembers().length);
|
||||
useEventEmitter(room.currentState, "RoomState.members", () => {
|
||||
setCount(room.getJoinedMembers().length);
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
|
@ -250,7 +244,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
const memberCount = useMemberCount(room);
|
||||
const memberCount = useRoomMemberCount(room);
|
||||
|
||||
return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
|
||||
|
|
|
@ -42,7 +42,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||
|
||||
const apps = useWidgets(room);
|
||||
const app = apps.find(a => a.id === widgetId);
|
||||
const isPinned = app && WidgetStore.instance.isPinned(app.id);
|
||||
const isPinned = app && WidgetStore.instance.isPinned(room.roomId, app.id);
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
|
|
|
@ -26,9 +26,9 @@ import classNames from 'classnames';
|
|||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import CallView from "../voip/CallView";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import CallViewForRoom from '../voip/CallViewForRoom';
|
||||
|
||||
interface IProps {
|
||||
// js-sdk room object
|
||||
|
@ -166,8 +166,8 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const callView = (
|
||||
<CallView
|
||||
room={this.props.room}
|
||||
<CallViewForRoom
|
||||
roomId={this.props.room.roomId}
|
||||
onResize={this.props.onResize}
|
||||
maxVideoHeight={this.props.maxHeight}
|
||||
/>
|
||||
|
|
|
@ -47,6 +47,7 @@ import AutocompleteWrapperModel from "../../../editor/autocomplete";
|
|||
import DocumentPosition from "../../../editor/position";
|
||||
import {ICompletion} from "../../../autocomplete/Autocompleter";
|
||||
|
||||
// matches emoticons which follow the start of a line or whitespace
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||
|
@ -524,7 +525,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
const range = model.startRange(position);
|
||||
range.expandBackwardsWhile((index, offset, part) => {
|
||||
return part.text[offset] !== " " && (
|
||||
return part.text[offset] !== " " && part.text[offset] !== "+" && (
|
||||
part.type === "plain" ||
|
||||
part.type === "pill-candidate" ||
|
||||
part.type === "command"
|
||||
|
|
|
@ -745,13 +745,22 @@ export default class EventTile extends React.Component {
|
|||
}
|
||||
|
||||
if (this.props.mxEvent.sender && avatarSize) {
|
||||
let member;
|
||||
// set member to receiver (target) if it is a 3PID invite
|
||||
// so that the correct avatar is shown as the text is
|
||||
// `$target accepted the invitation for $email`
|
||||
if (this.props.mxEvent.getContent().third_party_invite) {
|
||||
member = this.props.mxEvent.target;
|
||||
} else {
|
||||
member = this.props.mxEvent.sender;
|
||||
}
|
||||
avatar = (
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={this.props.mxEvent.sender}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={member}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -60,8 +60,9 @@ const NewRoomIntro = () => {
|
|||
{ caption && <p>{ caption }</p> }
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
const inRoom = room && room.getMyMembership() === "join";
|
||||
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
|
||||
const canAddTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||
|
||||
const onTopicClick = () => {
|
||||
dis.dispatch({
|
||||
|
@ -99,9 +100,25 @@ const NewRoomIntro = () => {
|
|||
});
|
||||
}
|
||||
|
||||
const onInviteClick = () => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
};
|
||||
let canInvite = inRoom;
|
||||
const powerLevels = room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||
const me = room.getMember(cli.getUserId());
|
||||
if (powerLevels && me && powerLevels.invite > me.powerLevel) {
|
||||
canInvite = false;
|
||||
}
|
||||
|
||||
let buttons;
|
||||
if (canInvite) {
|
||||
const onInviteClick = () => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
};
|
||||
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||
body = <React.Fragment>
|
||||
|
@ -119,11 +136,7 @@ const NewRoomIntro = () => {
|
|||
roomName: () => <b>{ room.name }</b>,
|
||||
})}</p>
|
||||
<p>{topicText}</p>
|
||||
<div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ buttons }
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,8 +42,12 @@ import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {containsEmoji} from "../../../effects/utils";
|
||||
import {CHAT_EFFECTS} from '../../../effects';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import EMOJI_REGEX from 'emojibase-regex';
|
||||
|
||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||
|
@ -89,6 +93,24 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) {
|
|||
return content;
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export function isQuickReaction(model) {
|
||||
const parts = model.parts;
|
||||
if (parts.length == 0) return false;
|
||||
const text = textSerialize(model);
|
||||
// shortcut takes the form "+:emoji:" or "+ :emoji:""
|
||||
// can be in 1 or 2 parts
|
||||
if (parts.length <= 2) {
|
||||
const hasShortcut = text.startsWith("+") || text.startsWith("+ ");
|
||||
const emojiMatch = text.match(EMOJI_REGEX);
|
||||
if (hasShortcut && emojiMatch && emojiMatch.length == 1) {
|
||||
return emojiMatch[0] === text.substring(1) ||
|
||||
emojiMatch[0] === text.substring(2);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default class SendMessageComposer extends React.Component {
|
||||
static propTypes = {
|
||||
room: PropTypes.object.isRequired,
|
||||
|
@ -221,6 +243,41 @@ export default class SendMessageComposer extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
_sendQuickReaction() {
|
||||
const timeline = this.props.room.getLiveTimeline();
|
||||
const events = timeline.getEvents();
|
||||
const reaction = this.model.parts[1].text;
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
if (events[i].getType() === "m.room.message") {
|
||||
let shouldReact = true;
|
||||
const lastMessage = events[i];
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const messageReactions = this.props.room.getUnfilteredTimelineSet()
|
||||
.getRelationsForEvent(lastMessage.getId(), "m.annotation", "m.reaction");
|
||||
|
||||
// if we have already sent this reaction, don't redact but don't re-send
|
||||
if (messageReactions) {
|
||||
const myReactionEvents = messageReactions.getAnnotationsBySender()[userId] || [];
|
||||
const myReactionKeys = [...myReactionEvents]
|
||||
.filter(event => !event.isRedacted())
|
||||
.map(event => event.getRelation().key);
|
||||
shouldReact = !myReactionKeys.includes(reaction);
|
||||
}
|
||||
if (shouldReact) {
|
||||
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": lastMessage.getId(),
|
||||
"key": reaction,
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: "message_sent"});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getSlashCommand() {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
|
@ -308,6 +365,11 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
if (isQuickReaction(this.model)) {
|
||||
shouldSend = false;
|
||||
this._sendQuickReaction();
|
||||
}
|
||||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
if (shouldSend) {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
|
@ -326,6 +388,11 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CHAT_EFFECTS.forEach((effect) => {
|
||||
if (containsEmoji(content, effect.emojis)) {
|
||||
dis.dispatch({action: `effects.${effect.command}`});
|
||||
}
|
||||
});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
}
|
||||
|
||||
|
|
|
@ -394,7 +394,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
|
||||
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
|
||||
>
|
||||
{this.state.showAdvanced ? "Hide advanced" : "Show advanced"}
|
||||
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
|
||||
</div>;
|
||||
|
||||
let advanced: React.ReactNode;
|
||||
|
|
|
@ -67,7 +67,6 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name={"advancedRoomListLogging"} level={SettingLevel.DEVICE} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -50,6 +50,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
'showChatEffects',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
];
|
||||
|
||||
|
|
|
@ -24,7 +24,8 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
|
@ -40,9 +41,50 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
roomId: string;
|
||||
activeCall: MatrixCall;
|
||||
|
||||
// The main call that we are displaying (ie. not including the call in the room being viewed, if any)
|
||||
primaryCall: MatrixCall;
|
||||
|
||||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||
// they belong to
|
||||
secondaryCall: MatrixCall;
|
||||
}
|
||||
|
||||
// Splits a list of calls into one 'primary' one and a list
|
||||
// (which should be a single element) of other calls.
|
||||
// The primary will be the one not on hold, or an arbitrary one
|
||||
// if they're all on hold)
|
||||
function getPrimarySecondaryCalls(calls: MatrixCall[]): [MatrixCall, MatrixCall[]] {
|
||||
let primary: MatrixCall = null;
|
||||
let secondaries: MatrixCall[] = [];
|
||||
|
||||
for (const call of calls) {
|
||||
if (!SHOW_CALL_IN_STATES.includes(call.state)) continue;
|
||||
|
||||
if (!call.isRemoteOnHold() && primary === null) {
|
||||
primary = call;
|
||||
} else {
|
||||
secondaries.push(call);
|
||||
}
|
||||
}
|
||||
|
||||
if (primary === null && secondaries.length > 0) {
|
||||
primary = secondaries[0];
|
||||
secondaries = secondaries.slice(1);
|
||||
}
|
||||
|
||||
if (secondaries.length > 1) {
|
||||
// We should never be in more than two calls so this shouldn't happen
|
||||
console.log("Found more than 1 secondary call! Other calls will not be shown.");
|
||||
}
|
||||
|
||||
return [primary, secondaries];
|
||||
}
|
||||
|
||||
/**
|
||||
* CallPreview shows a small version of CallView hovering over the UI in 'picture-in-picture'
|
||||
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing.
|
||||
*/
|
||||
export default class CallPreview extends React.Component<IProps, IState> {
|
||||
private roomStoreToken: any;
|
||||
private dispatcherRef: string;
|
||||
|
@ -51,18 +93,27 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(roomId),
|
||||
);
|
||||
|
||||
this.state = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
|
||||
roomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
|
@ -72,8 +123,16 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
|
||||
private onRoomViewStoreUpdate = (payload) => {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(roomId),
|
||||
);
|
||||
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
roomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -81,38 +140,35 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
switch (payload.action) {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
case 'call_state':
|
||||
case 'call_state': {
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||
);
|
||||
|
||||
this.setState({
|
||||
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onCallViewClick = () => {
|
||||
const call = CallHandler.sharedInstance().getAnyActiveCall();
|
||||
if (call) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: call.roomId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
|
||||
const showCall = (
|
||||
this.state.activeCall &&
|
||||
SHOW_CALL_IN_STATES.includes(this.state.activeCall.state) &&
|
||||
!callForRoom
|
||||
private onCallRemoteHold = () => {
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||
);
|
||||
|
||||
if (showCall) {
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.primaryCall) {
|
||||
return (
|
||||
<CallView
|
||||
onClick={this.onCallViewClick}
|
||||
showHangup={true}
|
||||
/>
|
||||
<CallView call={this.state.primaryCall} secondaryCall={this.state.secondaryCall} pipMode={true} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import React, { createRef, CSSProperties, ReactNode } from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
@ -28,33 +27,39 @@ import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
|||
import classNames from 'classnames';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
||||
import {aboveLeftOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu';
|
||||
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||
import { avatarUrlForMember } from '../../../Avatar';
|
||||
|
||||
interface IProps {
|
||||
// js-sdk room object. If set, we will only show calls for the given
|
||||
// room; if not, we will show any active call.
|
||||
room?: Room;
|
||||
// The call for us to display
|
||||
call: MatrixCall,
|
||||
|
||||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall,
|
||||
|
||||
// maxHeight style attribute for the video panel
|
||||
maxVideoHeight?: number;
|
||||
|
||||
// a callback which is called when the user clicks on the video div
|
||||
onClick?: React.MouseEventHandler;
|
||||
|
||||
// a callback which is called when the content in the callview changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
|
||||
// Whether to show the hang up icon:W
|
||||
showHangup?: boolean;
|
||||
// Whether this call view is for picture-in-pictue mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
pipMode?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
call: MatrixCall;
|
||||
isLocalOnHold: boolean,
|
||||
isRemoteOnHold: boolean,
|
||||
micMuted: boolean,
|
||||
vidMuted: boolean,
|
||||
callState: CallState,
|
||||
controlsVisible: boolean,
|
||||
showMoreMenu: boolean,
|
||||
}
|
||||
|
||||
function getFullScreenElement() {
|
||||
|
@ -89,25 +94,30 @@ const CONTROLS_HIDE_DELAY = 1000;
|
|||
// Height of the header duplicated from CSS because we need to subtract it from our max
|
||||
// height to get the max height of the video
|
||||
const HEADER_HEIGHT = 44;
|
||||
const BOTTOM_PADDING = 10;
|
||||
const BOTTOM_MARGIN_TOP_BOTTOM = 10; // top margin plus bottom margin
|
||||
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
|
||||
|
||||
export default class CallView extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private contentRef = createRef<HTMLDivElement>();
|
||||
private controlsHideTimer: number = null;
|
||||
private contextMenuButton = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const call = this.getCall();
|
||||
this.state = {
|
||||
call,
|
||||
isLocalOnHold: call ? call.isLocalOnHold() : null,
|
||||
micMuted: call ? call.isMicrophoneMuted() : null,
|
||||
vidMuted: call ? call.isLocalVideoMuted() : null,
|
||||
callState: call ? call.state : null,
|
||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||
isRemoteOnHold: this.props.call.isRemoteOnHold(),
|
||||
micMuted: this.props.call.isMicrophoneMuted(),
|
||||
vidMuted: this.props.call.isLocalVideoMuted(),
|
||||
callState: this.props.call.state,
|
||||
controlsVisible: true,
|
||||
showMoreMenu: false,
|
||||
}
|
||||
|
||||
this.updateCallListeners(null, call);
|
||||
this.updateCallListeners(null, this.props.call);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -116,11 +126,29 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (getFullScreenElement()) {
|
||||
exitFullscreen();
|
||||
}
|
||||
|
||||
document.removeEventListener("keydown", this.onNativeKeyDown);
|
||||
this.updateCallListeners(this.state.call, null);
|
||||
this.updateCallListeners(this.props.call, null);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps) {
|
||||
if (this.props.call === prevProps.call) return;
|
||||
|
||||
this.setState({
|
||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||
isRemoteOnHold: this.props.call.isRemoteOnHold(),
|
||||
micMuted: this.props.call.isMicrophoneMuted(),
|
||||
vidMuted: this.props.call.isLocalVideoMuted(),
|
||||
callState: this.props.call.state,
|
||||
});
|
||||
|
||||
this.updateCallListeners(null, this.props.call);
|
||||
}
|
||||
|
||||
private onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case 'video_fullscreen': {
|
||||
|
@ -134,66 +162,41 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'call_state': {
|
||||
const newCall = this.getCall();
|
||||
if (newCall !== this.state.call) {
|
||||
this.updateCallListeners(this.state.call, newCall);
|
||||
let newControlsVisible = this.state.controlsVisible;
|
||||
if (newCall && !this.state.call) {
|
||||
newControlsVisible = true;
|
||||
if (this.controlsHideTimer !== null) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
}
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
this.setState({
|
||||
call: newCall,
|
||||
isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
|
||||
micMuted: newCall ? newCall.isMicrophoneMuted() : null,
|
||||
vidMuted: newCall ? newCall.isLocalVideoMuted() : null,
|
||||
callState: newCall ? newCall.state : null,
|
||||
controlsVisible: newControlsVisible,
|
||||
});
|
||||
}
|
||||
if (!newCall && getFullScreenElement()) {
|
||||
exitFullscreen();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private getCall(): MatrixCall {
|
||||
let call: MatrixCall;
|
||||
|
||||
if (this.props.room) {
|
||||
const roomId = this.props.room.roomId;
|
||||
call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||
} else {
|
||||
call = CallHandler.sharedInstance().getAnyActiveCall();
|
||||
// Ignore calls if we can't get the room associated with them.
|
||||
// I think the underlying problem is that the js-sdk sends events
|
||||
// for calls before it has made the rooms available in the store,
|
||||
// although this isn't confirmed.
|
||||
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
|
||||
call = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
|
||||
return call;
|
||||
}
|
||||
|
||||
private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
|
||||
if (oldCall === newCall) return;
|
||||
|
||||
if (oldCall) oldCall.removeListener(CallEvent.HoldUnhold, this.onCallHoldUnhold);
|
||||
if (newCall) newCall.on(CallEvent.HoldUnhold, this.onCallHoldUnhold);
|
||||
if (oldCall) {
|
||||
oldCall.removeListener(CallEvent.State, this.onCallState);
|
||||
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||
}
|
||||
if (newCall) {
|
||||
newCall.on(CallEvent.State, this.onCallState);
|
||||
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||
}
|
||||
}
|
||||
|
||||
private onCallHoldUnhold = () => {
|
||||
private onCallState = (state) => {
|
||||
this.setState({
|
||||
isLocalOnHold: this.state.call ? this.state.call.isLocalOnHold() : null,
|
||||
callState: state,
|
||||
});
|
||||
};
|
||||
|
||||
private onCallLocalHoldUnhold = () => {
|
||||
this.setState({
|
||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||
});
|
||||
};
|
||||
|
||||
private onCallRemoteHoldUnhold = () => {
|
||||
this.setState({
|
||||
isRemoteOnHold: this.props.call.isRemoteOnHold(),
|
||||
// update both here because isLocalOnHold changes when we hold the call too
|
||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -207,7 +210,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
private onExpandClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.call.roomId,
|
||||
room_id: this.props.call.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -223,6 +226,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private showControls() {
|
||||
if (this.state.showMoreMenu) return;
|
||||
|
||||
if (!this.state.controlsVisible) {
|
||||
this.setState({
|
||||
controlsVisible: true,
|
||||
|
@ -235,23 +240,38 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onMicMuteClick = () => {
|
||||
if (!this.state.call) return;
|
||||
|
||||
const newVal = !this.state.micMuted;
|
||||
|
||||
this.state.call.setMicrophoneMuted(newVal);
|
||||
this.props.call.setMicrophoneMuted(newVal);
|
||||
this.setState({micMuted: newVal});
|
||||
}
|
||||
|
||||
private onVidMuteClick = () => {
|
||||
if (!this.state.call) return;
|
||||
|
||||
const newVal = !this.state.vidMuted;
|
||||
|
||||
this.state.call.setLocalVideoMuted(newVal);
|
||||
this.props.call.setLocalVideoMuted(newVal);
|
||||
this.setState({vidMuted: newVal});
|
||||
}
|
||||
|
||||
private onMoreClick = () => {
|
||||
if (this.controlsHideTimer) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
this.controlsHideTimer = null;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showMoreMenu: true,
|
||||
controlsVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
private closeContextMenu = () => {
|
||||
this.setState({
|
||||
showMoreMenu: false,
|
||||
});
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
|
||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||
// Note that this assumes we always have a callview on screen at any given time
|
||||
// CallHandler would probably be a better place for this
|
||||
|
@ -288,114 +308,219 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
private onRoomAvatarClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.call.roomId,
|
||||
room_id: this.props.call.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onSecondaryRoomAvatarClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.secondaryCall.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onCallResumeClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
|
||||
}
|
||||
|
||||
private onSecondaryCallResumeClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.secondaryCall.roomId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.state.call) return null;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoom = client.getRoom(this.state.call.roomId);
|
||||
const callRoom = client.getRoom(this.props.call.roomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null;
|
||||
|
||||
let callControls;
|
||||
if (this.props.room) {
|
||||
const micClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_micOn: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_micOff: this.state.micMuted,
|
||||
});
|
||||
let contextMenu;
|
||||
|
||||
const vidClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_vidOn: !this.state.vidMuted,
|
||||
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
|
||||
});
|
||||
|
||||
// Put the other states of the mic/video icons in the document to make sure they're cached
|
||||
// (otherwise the icon disappears briefly when toggled)
|
||||
const micCacheClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_micOn: this.state.micMuted,
|
||||
mx_CallView_callControls_button_micOff: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_invisible: true,
|
||||
});
|
||||
|
||||
const vidCacheClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_vidOn: this.state.micMuted,
|
||||
mx_CallView_callControls_button_vidOff: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_invisible: true,
|
||||
});
|
||||
|
||||
const callControlsClasses = classNames({
|
||||
mx_CallView_callControls: true,
|
||||
mx_CallView_callControls_hidden: !this.state.controlsVisible,
|
||||
});
|
||||
|
||||
const vidMuteButton = this.state.call.type === CallType.Video ? <div
|
||||
className={vidClasses}
|
||||
onClick={this.onVidMuteClick}
|
||||
/> : null;
|
||||
|
||||
callControls = <div className={callControlsClasses}>
|
||||
<div
|
||||
className={micClasses}
|
||||
onClick={this.onMicMuteClick}
|
||||
/>
|
||||
<div
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.state.call.roomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{vidMuteButton}
|
||||
<div className={micCacheClasses} />
|
||||
<div className={vidCacheClasses} />
|
||||
</div>;
|
||||
if (this.state.showMoreMenu) {
|
||||
contextMenu = <CallContextMenu
|
||||
{...aboveLeftOf(
|
||||
this.contextMenuButton.current.getBoundingClientRect(),
|
||||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
)}
|
||||
onFinished={this.closeContextMenu}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
}
|
||||
|
||||
const micClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_micOn: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_micOff: this.state.micMuted,
|
||||
});
|
||||
|
||||
const vidClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_vidOn: !this.state.vidMuted,
|
||||
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
|
||||
});
|
||||
|
||||
// Put the other states of the mic/video icons in the document to make sure they're cached
|
||||
// (otherwise the icon disappears briefly when toggled)
|
||||
const micCacheClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_micOn: this.state.micMuted,
|
||||
mx_CallView_callControls_button_micOff: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_invisible: true,
|
||||
});
|
||||
|
||||
const vidCacheClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_vidOn: this.state.micMuted,
|
||||
mx_CallView_callControls_button_vidOff: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_invisible: true,
|
||||
});
|
||||
|
||||
const callControlsClasses = classNames({
|
||||
mx_CallView_callControls: true,
|
||||
mx_CallView_callControls_hidden: !this.state.controlsVisible,
|
||||
});
|
||||
|
||||
const vidMuteButton = this.props.call.type === CallType.Video ? <AccessibleButton
|
||||
className={vidClasses}
|
||||
onClick={this.onVidMuteClick}
|
||||
/> : null;
|
||||
|
||||
// The 'more' button actions are only relevant in a connected call
|
||||
// When not connected, we have to put something there to make the flexbox alignment correct
|
||||
const contextMenuButton = this.state.callState === CallState.Connected ? <ContextMenuButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
||||
onClick={this.onMoreClick}
|
||||
inputRef={this.contextMenuButton}
|
||||
isExpanded={this.state.showMoreMenu}
|
||||
/> : <div className="mx_CallView_callControls_button mx_CallView_callControls_button_more_hidden" />;
|
||||
|
||||
// in the near future, the dial pad button will go on the left. For now, it's the nothing button
|
||||
// because something needs to have margin-right: auto to make the alignment correct.
|
||||
const callControls = <div className={callControlsClasses}>
|
||||
<div className="mx_CallView_callControls_button mx_CallView_callControls_nothing" />
|
||||
<AccessibleButton
|
||||
className={micClasses}
|
||||
onClick={this.onMicMuteClick}
|
||||
/>
|
||||
<AccessibleButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.props.call.roomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{vidMuteButton}
|
||||
<div className={micCacheClasses} />
|
||||
<div className={vidCacheClasses} />
|
||||
{contextMenuButton}
|
||||
</div>;
|
||||
|
||||
// The 'content' for the call, ie. the videos for a video call and profile picture
|
||||
// for voice calls (fills the bg)
|
||||
let contentView: React.ReactNode;
|
||||
|
||||
if (this.state.call.type === CallType.Video) {
|
||||
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
|
||||
let onHoldText = null;
|
||||
if (this.state.isRemoteOnHold) {
|
||||
onHoldText = _t("You held the call <a>Resume</a>", {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
|
||||
{sub}
|
||||
</AccessibleButton>,
|
||||
});
|
||||
} else if (this.state.isLocalOnHold) {
|
||||
onHoldText = _t("%(peerName)s held the call", {
|
||||
peerName: this.props.call.getOpponentMember().name,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
let onHoldContent = null;
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
const containerClasses = classNames({
|
||||
mx_CallView_video: true,
|
||||
mx_CallView_video_hold: isOnHold,
|
||||
});
|
||||
if (isOnHold) {
|
||||
onHoldContent = <div className="mx_CallView_video_holdContent">
|
||||
{onHoldText}
|
||||
</div>;
|
||||
const backgroundAvatarUrl = avatarUrlForMember(
|
||||
// is it worth getting the size of the div to pass here?
|
||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||
);
|
||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||
}
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight - HEADER_HEIGHT;
|
||||
contentView = <div className="mx_CallView_video" ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
|
||||
const maxVideoHeight = getFullScreenElement() ? null : (
|
||||
this.props.maxVideoHeight - (HEADER_HEIGHT + BOTTOM_PADDING + BOTTOM_MARGIN_TOP_BOTTOM)
|
||||
);
|
||||
contentView = <div className={containerClasses}
|
||||
ref={this.contentRef} onMouseMove={this.onMouseMove}
|
||||
// Put the max height on here too because this div is ended up 4px larger than the content
|
||||
// and is causing it to scroll, and I am genuinely baffled as to why.
|
||||
style={{maxHeight: maxVideoHeight}}
|
||||
>
|
||||
{onHoldBackground}
|
||||
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize}
|
||||
maxHeight={maxVideoHeight}
|
||||
/>
|
||||
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
|
||||
<VideoFeed type={VideoFeedType.Local} call={this.props.call} />
|
||||
{onHoldContent}
|
||||
{callControls}
|
||||
</div>;
|
||||
} else {
|
||||
const avatarSize = this.props.room ? 200 : 75;
|
||||
contentView = <div className="mx_CallView_voice" onMouseMove={this.onMouseMove}>
|
||||
<RoomAvatar
|
||||
room={callRoom}
|
||||
height={avatarSize}
|
||||
width={avatarSize}
|
||||
/>
|
||||
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||
const classes = classNames({
|
||||
mx_CallView_voice: true,
|
||||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
let secondaryCallAvatar: ReactNode;
|
||||
|
||||
if (this.props.secondaryCall) {
|
||||
const secAvatarSize = this.props.pipMode ? 40 : 100;
|
||||
secondaryCallAvatar = <div className="mx_CallView_voice_secondaryAvatarContainer"
|
||||
style={{width: secAvatarSize, height: secAvatarSize}}
|
||||
>
|
||||
<RoomAvatar
|
||||
room={secCallRoom}
|
||||
height={secAvatarSize}
|
||||
width={secAvatarSize}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
|
||||
<RoomAvatar
|
||||
room={callRoom}
|
||||
height={avatarSize}
|
||||
width={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
{secondaryCallAvatar}
|
||||
</div>
|
||||
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
|
||||
{callControls}
|
||||
</div>;
|
||||
}
|
||||
|
||||
const callTypeText = this.state.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
|
||||
const callTypeText = this.props.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
|
||||
let myClassName;
|
||||
|
||||
let fullScreenButton;
|
||||
if (this.state.call.type === CallType.Video && this.props.room) {
|
||||
if (this.props.call.type === CallType.Video && !this.props.pipMode) {
|
||||
fullScreenButton = <div className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
||||
onClick={this.onFullscreenClick} title={_t("Fill Screen")}
|
||||
/>;
|
||||
}
|
||||
|
||||
let expandButton;
|
||||
if (!this.props.room) {
|
||||
if (this.props.pipMode) {
|
||||
expandButton = <div className="mx_CallView_header_button mx_CallView_header_button_expand"
|
||||
onClick={this.onExpandClick} title={_t("Return to call")}
|
||||
/>;
|
||||
|
@ -407,7 +532,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
</div>;
|
||||
|
||||
let header: React.ReactNode;
|
||||
if (this.props.room) {
|
||||
if (!this.props.pipMode) {
|
||||
header = <div className="mx_CallView_header">
|
||||
<div className="mx_CallView_header_phoneIcon"></div>
|
||||
<span className="mx_CallView_header_callType">{callTypeText}</span>
|
||||
|
@ -415,13 +540,28 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
</div>;
|
||||
myClassName = 'mx_CallView_large';
|
||||
} else {
|
||||
let secondaryCallInfo;
|
||||
if (this.props.secondaryCall) {
|
||||
secondaryCallInfo = <span className="mx_CallView_header_secondaryCallInfo">
|
||||
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
||||
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
||||
<span className="mx_CallView_secondaryCall_roomName">
|
||||
{_t("%(name)s paused", { name: secCallRoom.name })}
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
}
|
||||
|
||||
header = <div className="mx_CallView_header">
|
||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||
</AccessibleButton>
|
||||
<div>
|
||||
<div className="mx_CallView_header_callInfo">
|
||||
<div className="mx_CallView_header_roomName">{callRoom.name}</div>
|
||||
<div className="mx_CallView_header_callTypeSmall">{callTypeText}</div>
|
||||
<div className="mx_CallView_header_callTypeSmall">
|
||||
{callTypeText}
|
||||
{secondaryCallInfo}
|
||||
</div>
|
||||
</div>
|
||||
{headerControls}
|
||||
</div>;
|
||||
|
@ -431,6 +571,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
return <div className={"mx_CallView " + myClassName}>
|
||||
{header}
|
||||
{contentView}
|
||||
{contextMenu}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
87
src/components/views/voip/CallViewForRoom.tsx
Normal file
87
src/components/views/voip/CallViewForRoom.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import React from 'react';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import CallView from './CallView';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
||||
interface IProps {
|
||||
// What room we should display the call for
|
||||
roomId: string,
|
||||
|
||||
// maxHeight style attribute for the video panel
|
||||
maxVideoHeight?: number;
|
||||
|
||||
// a callback which is called when the content in the callview changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
call: MatrixCall,
|
||||
}
|
||||
|
||||
/*
|
||||
* Wrapper for CallView that always display the call in a given room,
|
||||
* or nothing if there is no call in that room.
|
||||
*/
|
||||
export default class CallViewForRoom extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
call: this.getCall(),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
private onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case 'call_state': {
|
||||
const newCall = this.getCall();
|
||||
if (newCall !== this.state.call) {
|
||||
this.setState({call: newCall});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private getCall(): MatrixCall {
|
||||
const call = CallHandler.sharedInstance().getCallForRoom(this.props.roomId);
|
||||
|
||||
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
|
||||
return call;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.state.call) return null;
|
||||
|
||||
return <CallView call={this.state.call} pipMode={false}
|
||||
onResize={this.props.onResize} maxVideoHeight={this.props.maxVideoHeight}
|
||||
/>;
|
||||
}
|
||||
}
|
|
@ -42,10 +42,12 @@ export default class VideoFeed extends React.Component<IProps> {
|
|||
|
||||
componentDidMount() {
|
||||
this.vid.current.addEventListener('resize', this.onResize);
|
||||
if (this.props.type === VideoFeedType.Local) {
|
||||
this.props.call.setLocalVideoElement(this.vid.current);
|
||||
} else {
|
||||
this.props.call.setRemoteVideoElement(this.vid.current);
|
||||
this.setVideoElement();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.call !== prevProps.call) {
|
||||
this.setVideoElement();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,6 +55,14 @@ export default class VideoFeed extends React.Component<IProps> {
|
|||
this.vid.current.removeEventListener('resize', this.onResize);
|
||||
}
|
||||
|
||||
private setVideoElement() {
|
||||
if (this.props.type === VideoFeedType.Local) {
|
||||
this.props.call.setLocalVideoElement(this.vid.current);
|
||||
} else {
|
||||
this.props.call.setRemoteVideoElement(this.vid.current);
|
||||
}
|
||||
}
|
||||
|
||||
onResize = (e) => {
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize(e);
|
||||
|
|
45
src/customisations/RoomList.ts
Normal file
45
src/customisations/RoomList.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
// Populate this file with the details of your customisations when copying it.
|
||||
|
||||
/**
|
||||
* Determines if a room is visible in the room list or not. By default,
|
||||
* all rooms are visible. Where special handling is performed by Element,
|
||||
* those rooms will not be able to override their visibility in the room
|
||||
* list - Element will make the decision without calling this function.
|
||||
*
|
||||
* This function should be as fast as possible to avoid slowing down the
|
||||
* client.
|
||||
* @param {Room} room The room to check the visibility of.
|
||||
* @returns {boolean} True if the room should be visible, false otherwise.
|
||||
*/
|
||||
function isRoomVisible(room: Room): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// This interface summarises all available customisation points and also marks
|
||||
// them all as optional. This allows customisers to only define and export the
|
||||
// customisations they need while still maintaining type safety.
|
||||
export interface IRoomListCustomisations {
|
||||
isRoomVisible?: typeof isRoomVisible;
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
// customisation points that make up the interface above.
|
||||
export const RoomListCustomisations: IRoomListCustomisations = {};
|
|
@ -190,7 +190,9 @@ abstract class PlainBasePart extends BasePart {
|
|||
return true;
|
||||
}
|
||||
// only split if the previous character is a space
|
||||
return this._text[offset - 1] !== " ";
|
||||
// or if it is a + and this is a :
|
||||
return this._text[offset - 1] !== " " &&
|
||||
(this._text[offset - 1] !== "+" || chr !== ":");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
47
src/effects/ICanvasEffect.ts
Normal file
47
src/effects/ICanvasEffect.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2020 Nurjin Jafar
|
||||
Copyright 2020 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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.
|
||||
*/
|
||||
/**
|
||||
* Defines the constructor of a canvas based room effect
|
||||
*/
|
||||
export interface ICanvasEffectConstructable {
|
||||
/**
|
||||
* @param {{[key:string]:any}} options? Optional animation options
|
||||
* @returns ICanvasEffect Returns a new instance of the canvas effect
|
||||
*/
|
||||
new(options?: { [key: string]: any }): ICanvasEffect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the interface of a canvas based room effect
|
||||
*/
|
||||
export default interface ICanvasEffect {
|
||||
/**
|
||||
* @param {HTMLCanvasElement} canvas The canvas instance as the render target of the animation
|
||||
* @param {number} timeout? A timeout that defines the runtime of the animation (defaults to false)
|
||||
*/
|
||||
start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Stops the current animation
|
||||
*/
|
||||
stop: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns a value that defines if the animation is currently running
|
||||
*/
|
||||
isRunning: boolean;
|
||||
}
|
191
src/effects/confetti/index.ts
Normal file
191
src/effects/confetti/index.ts
Normal file
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
Copyright 2020 Nurjin Jafar
|
||||
Copyright 2020 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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 ICanvasEffect from '../ICanvasEffect';
|
||||
|
||||
export type ConfettiOptions = {
|
||||
/**
|
||||
* max confetti count
|
||||
*/
|
||||
maxCount: number,
|
||||
/**
|
||||
* particle animation speed
|
||||
*/
|
||||
speed: number,
|
||||
/**
|
||||
* the confetti animation frame interval in milliseconds
|
||||
*/
|
||||
frameInterval: number,
|
||||
/**
|
||||
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
|
||||
*/
|
||||
alpha: number,
|
||||
/**
|
||||
* use gradient instead of solid particle color
|
||||
*/
|
||||
gradient: boolean,
|
||||
}
|
||||
|
||||
type ConfettiParticle = {
|
||||
color: string,
|
||||
color2: string,
|
||||
x: number,
|
||||
y: number,
|
||||
diameter: number,
|
||||
tilt: number,
|
||||
tiltAngleIncrement: number,
|
||||
tiltAngle: number,
|
||||
}
|
||||
|
||||
export const DefaultOptions: ConfettiOptions = {
|
||||
maxCount: 150,
|
||||
speed: 3,
|
||||
frameInterval: 15,
|
||||
alpha: 1.0,
|
||||
gradient: false,
|
||||
};
|
||||
|
||||
export default class Confetti implements ICanvasEffect {
|
||||
private readonly options: ConfettiOptions;
|
||||
|
||||
constructor(options: { [key: string]: any }) {
|
||||
this.options = {...DefaultOptions, ...options};
|
||||
}
|
||||
|
||||
private context: CanvasRenderingContext2D | null = null;
|
||||
private supportsAnimationFrame = window.requestAnimationFrame;
|
||||
private colors = ['rgba(30,144,255,', 'rgba(107,142,35,', 'rgba(255,215,0,',
|
||||
'rgba(255,192,203,', 'rgba(106,90,205,', 'rgba(173,216,230,',
|
||||
'rgba(238,130,238,', 'rgba(152,251,152,', 'rgba(70,130,180,',
|
||||
'rgba(244,164,96,', 'rgba(210,105,30,', 'rgba(220,20,60,'];
|
||||
|
||||
private lastFrameTime = Date.now();
|
||||
private particles: Array<ConfettiParticle> = [];
|
||||
private waveAngle = 0;
|
||||
|
||||
public isRunning: boolean;
|
||||
|
||||
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
this.context = canvas.getContext('2d');
|
||||
this.particles = [];
|
||||
const count = this.options.maxCount;
|
||||
while (this.particles.length < count) {
|
||||
this.particles.push(this.resetParticle({} as ConfettiParticle, canvas.width, canvas.height));
|
||||
}
|
||||
this.isRunning = true;
|
||||
this.runAnimation();
|
||||
if (timeout) {
|
||||
window.setTimeout(this.stop, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
public stop = async () => {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
|
||||
particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
|
||||
if (this.options.gradient) {
|
||||
particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
|
||||
} else {
|
||||
particle.color2 = particle.color;
|
||||
}
|
||||
particle.x = Math.random() * width;
|
||||
particle.y = Math.random() * -height;
|
||||
particle.diameter = Math.random() * 10 + 5;
|
||||
particle.tilt = Math.random() * -10;
|
||||
particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
|
||||
particle.tiltAngle = Math.random() * Math.PI;
|
||||
return particle;
|
||||
}
|
||||
|
||||
private runAnimation = (): void => {
|
||||
if (!this.context || !this.context.canvas) {
|
||||
return;
|
||||
}
|
||||
if (this.particles.length === 0) {
|
||||
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||
} else {
|
||||
const now = Date.now();
|
||||
const delta = now - this.lastFrameTime;
|
||||
if (!this.supportsAnimationFrame || delta > this.options.frameInterval) {
|
||||
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
||||
this.updateParticles();
|
||||
this.drawParticles(this.context);
|
||||
this.lastFrameTime = now - (delta % this.options.frameInterval);
|
||||
}
|
||||
requestAnimationFrame(this.runAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private drawParticles = (context: CanvasRenderingContext2D): void => {
|
||||
if (!this.context || !this.context.canvas) {
|
||||
return;
|
||||
}
|
||||
let x; let x2; let y2;
|
||||
for (const particle of this.particles) {
|
||||
this.context.beginPath();
|
||||
context.lineWidth = particle.diameter;
|
||||
x2 = particle.x + particle.tilt;
|
||||
x = x2 + particle.diameter / 2;
|
||||
y2 = particle.y + particle.tilt + particle.diameter / 2;
|
||||
if (this.options.gradient) {
|
||||
const gradient = context.createLinearGradient(x, particle.y, x2, y2);
|
||||
gradient.addColorStop(0, particle.color);
|
||||
gradient.addColorStop(1.0, particle.color2);
|
||||
context.strokeStyle = gradient;
|
||||
} else {
|
||||
context.strokeStyle = particle.color;
|
||||
}
|
||||
context.moveTo(x, particle.y);
|
||||
context.lineTo(x2, y2);
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
private updateParticles = () => {
|
||||
if (!this.context || !this.context.canvas) {
|
||||
return;
|
||||
}
|
||||
const width = this.context.canvas.width;
|
||||
const height = this.context.canvas.height;
|
||||
let particle: ConfettiParticle;
|
||||
this.waveAngle += 0.01;
|
||||
for (let i = 0; i < this.particles.length; i++) {
|
||||
particle = this.particles[i];
|
||||
if (!this.isRunning && particle.y < -15) {
|
||||
particle.y = height + 100;
|
||||
} else {
|
||||
particle.tiltAngle += particle.tiltAngleIncrement;
|
||||
particle.x += Math.sin(this.waveAngle) - 0.5;
|
||||
particle.y += (Math.cos(this.waveAngle) + particle.diameter + this.options.speed) * 0.5;
|
||||
particle.tilt = Math.sin(particle.tiltAngle) * 15;
|
||||
}
|
||||
if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
|
||||
if (this.isRunning && this.particles.length <= this.options.maxCount) {
|
||||
this.resetParticle(particle, width, height);
|
||||
} else {
|
||||
this.particles.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
89
src/effects/index.ts
Normal file
89
src/effects/index.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
Copyright 2020 Nurjin Jafar
|
||||
Copyright 2020 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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 { _t, _td } from "../languageHandler";
|
||||
|
||||
export type Effect<TOptions extends { [key: string]: any }> = {
|
||||
/**
|
||||
* one or more emojis that will trigger this effect
|
||||
*/
|
||||
emojis: Array<string>;
|
||||
/**
|
||||
* the matrix message type that will trigger this effect
|
||||
*/
|
||||
msgType: string;
|
||||
/**
|
||||
* the room command to trigger this effect
|
||||
*/
|
||||
command: string;
|
||||
/**
|
||||
* a function that returns the translated description of the effect
|
||||
*/
|
||||
description: () => string;
|
||||
/**
|
||||
* a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
|
||||
*/
|
||||
fallbackMessage: () => string;
|
||||
/**
|
||||
* animation options
|
||||
*/
|
||||
options: TOptions;
|
||||
}
|
||||
|
||||
type ConfettiOptions = {
|
||||
/**
|
||||
* max confetti count
|
||||
*/
|
||||
maxCount: number,
|
||||
/**
|
||||
* particle animation speed
|
||||
*/
|
||||
speed: number,
|
||||
/**
|
||||
* the confetti animation frame interval in milliseconds
|
||||
*/
|
||||
frameInterval: number,
|
||||
/**
|
||||
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
|
||||
*/
|
||||
alpha: number,
|
||||
/**
|
||||
* use gradient instead of solid particle color
|
||||
*/
|
||||
gradient: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* This configuration defines room effects that can be triggered by custom message types and emojis
|
||||
*/
|
||||
export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
|
||||
{
|
||||
emojis: ['🎊', '🎉'],
|
||||
msgType: 'nic.custom.confetti',
|
||||
command: 'confetti',
|
||||
description: () => _td("Sends the given message with confetti"),
|
||||
fallbackMessage: () => _t("sends confetti") + " 🎉",
|
||||
options: {
|
||||
maxCount: 150,
|
||||
speed: 3,
|
||||
frameInterval: 15,
|
||||
alpha: 1.0,
|
||||
gradient: false,
|
||||
},
|
||||
} as Effect<ConfettiOptions>,
|
||||
];
|
||||
|
||||
|
24
src/effects/utils.ts
Normal file
24
src/effects/utils.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
Copyright 2020 Nurjin Jafar
|
||||
Copyright 2020 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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.
|
||||
*/
|
||||
/**
|
||||
* Checks a message if it contains one of the provided emojis
|
||||
* @param {Object} content The message
|
||||
* @param {Array<string>} emojis The list of emojis to check for
|
||||
*/
|
||||
export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array<string>): boolean => {
|
||||
return emojis.some((emoji) => content.body && content.body.includes(emoji));
|
||||
}
|
40
src/hooks/useRoomMembers.ts
Normal file
40
src/hooks/useRoomMembers.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import {useEventEmitter} from "./useEventEmitter";
|
||||
import {throttle} from "lodash";
|
||||
|
||||
// Hook to simplify watching Matrix Room joined members
|
||||
export const useRoomMembers = (room: Room, throttleWait = 250) => {
|
||||
const [members, setMembers] = useState<RoomMember[]>(room.getJoinedMembers());
|
||||
useEventEmitter(room.currentState, "RoomState.members", throttle(() => {
|
||||
setMembers(room.getJoinedMembers());
|
||||
}, throttleWait, {leading: true, trailing: true}));
|
||||
return members;
|
||||
};
|
||||
|
||||
// Hook to simplify watching Matrix Room joined member count
|
||||
export const useRoomMemberCount = (room: Room, throttleWait = 250) => {
|
||||
const [count, setCount] = useState<number>(room.getJoinedMemberCount());
|
||||
useEventEmitter(room.currentState, "RoomState.members", throttle(() => {
|
||||
setCount(room.getJoinedMemberCount());
|
||||
}, throttleWait, {leading: true, trailing: true}));
|
||||
return count;
|
||||
};
|
|
@ -1422,5 +1422,71 @@
|
|||
"This will end the conference for everyone. Continue?": "هذا سينهي المؤتمر للجميع. استمر؟",
|
||||
"The call was answered on another device.": "تم الرد على المكالمة على جهاز آخر.",
|
||||
"The call could not be established": "تعذر إجراء المكالمة",
|
||||
"Call Declined": "رُفض الاتصال"
|
||||
"Call Declined": "رُفض الاتصال",
|
||||
"See videos posted to your active room": "أظهر الفيديوهات المرسلة إلى هذه غرفتك النشطة",
|
||||
"See videos posted to this room": "أظهر الفيديوهات المرسلة إلى هذه الغرفة",
|
||||
"Send videos as you in your active room": "أرسل الفيديوهات بهويتك في غرفتك النشطة",
|
||||
"Send videos as you in this room": "أرسل الفيديوهات بهويتك في هذه الغرفة",
|
||||
"See messages posted to this room": "أرسل الرسائل المرسلة إلى هذه الغرفة",
|
||||
"See images posted to your active room": "أظهر الصور المرسلة إلى غرفتك النشطة",
|
||||
"See images posted to this room": "أظهر الصور المرسلة إلى هذه الغرفة",
|
||||
"Send images as you in your active room": "أرسل الصور بهويتك في غرفتك النشطة",
|
||||
"Send images as you in this room": "أرسل الصور بهويتك في هذه الغرفة",
|
||||
"See emotes posted to your active room": "أظهر الرموز التعبيرية المرسلة لغرفتك النشطة",
|
||||
"See emotes posted to this room": "أظهر الرموز التعبيرية المرسلة إلى هذه الغرفة",
|
||||
"Send emotes as you in your active room": "أظهر الرموز التعبيرية بهويتك في غرفتك النشطة",
|
||||
"Send emotes as you in this room": "أرسل الرموز التعبيرية بهويتك",
|
||||
"See text messages posted to your active room": "أظهر الرسائل النصية المرسلة إلى غرفتك النشطة",
|
||||
"See text messages posted to this room": "أظهر الرسائل النصية المرسلة إلى هذه الغرفة",
|
||||
"Send text messages as you in your active room": "أرسل الرسائل النصية بهويتك في غرفتك النشطة",
|
||||
"Send text messages as you in this room": "أرسل الرسائل النصية بهويتك في هذه الغرفة",
|
||||
"See messages posted to your active room": "أظهر الرسائل المرسلة إلى غرفتك النشطة",
|
||||
"Send messages as you in your active room": "أرسل رسائل بهويتك في غرفتك النشطة",
|
||||
"Send messages as you in this room": "أرسل رسائل بهويتك في هذه الغرفة",
|
||||
"The <b>%(capability)s</b> capability": "القدرة <b>%(capability)s</b>",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "أظهر أحداث <b>%(eventType)s</b> المنشورة بغرفة النشطة",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "أرسل أحداث <b>%(eventType)s</b> بهويتك في غرفتك النشطة",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "أظهر أحداث <b>%(eventType)s</b> المنشورة في هذه الغرفة",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "أرسل أحداث <b>%(eventType)s</b> بهويتك في هذه الغرفة",
|
||||
"with state key %(stateKey)s": "مع مفتاح الحالة %(stateKey)s",
|
||||
"with an empty state key": "بمفتاح حالة فارغ",
|
||||
"See when anyone posts a sticker to your active room": "أظهر وضع أي أحد للملصقات لغرفتك النشطة",
|
||||
"Send stickers to your active room as you": "أرسل ملصقات لغرفتك النشطة بهويتك",
|
||||
"See when a sticker is posted in this room": "أظهر وضع الملصقات في هذه الغرفة",
|
||||
"Send stickers to this room as you": "أرسل ملصقات لهذه الغرفة بهويتك",
|
||||
"See when the avatar changes in your active room": "أظهر تغييرات صورة غرفتك النشطة",
|
||||
"Change the avatar of your active room": "غير صورة غرفتك النشطة",
|
||||
"See when the avatar changes in this room": "أظهر تغييرات الصورة في هذه الغرفة",
|
||||
"Change the avatar of this room": "غير صورة هذه الغرفة",
|
||||
"See when the name changes in your active room": "أظهر تغييرات الاسم في غرفتك النشطة",
|
||||
"Change the name of your active room": "غير اسم غرفتك النشطة",
|
||||
"See when the name changes in this room": "أظهر تغييرات الاسم في هذه الغرفة",
|
||||
"Change the name of this room": "غير اسم هذه الغرفة",
|
||||
"See when the topic changes in your active room": "أظهر تغيير موضوع غرفتك النشطة",
|
||||
"Change the topic of your active room": "غير موضوع غرفتك النشطة",
|
||||
"See when the topic changes in this room": "أظهر تغير موضوع هذه الغرفة",
|
||||
"Change the topic of this room": "تغيير موضوع هذه الغرفة",
|
||||
"Change which room you're viewing": "تغيير الغرفة التي تشاهدها",
|
||||
"Send stickers into your active room": "أرسل ملصقات إلى غرفتك النشطة",
|
||||
"Send stickers into this room": "أرسل ملصقات إلى هذه الغرفة",
|
||||
"Remain on your screen while running": "ابقَ على شاشتك أثناء إجراء",
|
||||
"Remain on your screen when viewing another room, when running": "ابقَ على شاشتك عند مشاهدة غرفة أخرى أثناء إجراء",
|
||||
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر المستخدمين المطابقة %(glob)s بسبب %(reason)s",
|
||||
"%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر الخوادم المطابقة %(glob)s بسبب %(reason)s",
|
||||
"%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر الغرفة المطابقة %(glob)s بسبب %(reason)s",
|
||||
"%(senderName)s declined the call.": "%(senderName)s رفض المكالمة.",
|
||||
"(an error occurred)": "(حدث خطأ)",
|
||||
"(their device couldn't start the camera / microphone)": "(تعذر على جهازهم بدء تشغيل الكاميرا / الميكروفون)",
|
||||
"(connection failed)": "(فشل الاتصال)",
|
||||
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 جميع الخوادم ممنوعة من المشاركة! لم يعد من الممكن استخدام هذه الغرفة.",
|
||||
"Takes the call in the current room off hold": "يوقف المكالمة في الغرفة الحالية",
|
||||
"Places the call in the current room on hold": "يضع المكالمة في الغرفة الحالية قيد الانتظار",
|
||||
"Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "يلصق (͡ ° ͜ʖ ͡ °) أوَّل رسالة نصية عادية",
|
||||
"No other application is using the webcam": "لا يوجد تطبيق آخر يستخدم كاميرا الويب",
|
||||
"Permission is granted to use the webcam": "منح الإذن باستخدام كاميرا الويب",
|
||||
"A microphone and webcam are plugged in and set up correctly": "الميكروفون وكاميرا ويب موصولان ومعدان بشكل صحيح",
|
||||
"Call failed because no webcam or microphone could not be accessed. Check that:": "فشلت المكالمة نظرًا لتعذر الوصول إلى كاميرا الويب أو الميكروفون. تحقق مما يلي:",
|
||||
"Unable to access webcam / microphone": "تعذر الوصول إلى كاميرا الويب / الميكروفون",
|
||||
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لأنه لا يمكن الوصول إلى ميكروفون. تحقق من توصيل الميكروفون وإعداده بشكل صحيح.",
|
||||
"Unable to access microphone": "تعذر الوصول إلى الميكروفون"
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@
|
|||
"Error: Problem communicating with the given homeserver.": "Chyba: problém v komunikaci s daným domovským serverem.",
|
||||
"Existing Call": "Probíhající hovor",
|
||||
"Export": "Exportovat",
|
||||
"Export E2E room keys": "Exportovat end-to-end klíče místnosti",
|
||||
"Export E2E room keys": "Exportovat end-to-end klíče místností",
|
||||
"Failed to ban user": "Nepodařilo se vykázat uživatele",
|
||||
"Failed to join room": "Vstup do místnosti se nezdařil",
|
||||
"Failed to kick": "Vykopnutí se nezdařilo",
|
||||
|
@ -131,14 +131,14 @@
|
|||
"Failed to send request.": "Odeslání žádosti se nezdařilo.",
|
||||
"Failed to set display name": "Nepodařilo se nastavit zobrazované jméno",
|
||||
"Failed to unban": "Přijetí zpět se nezdařilo",
|
||||
"Failed to upload profile picture!": "Nahrání profilového obrázku se nezdařilo",
|
||||
"Failed to upload profile picture!": "Nahrání profilového obrázku se nezdařilo!",
|
||||
"Failure to create room": "Vytvoření místnosti se nezdařilo",
|
||||
"Forget room": "Zapomenout místnost",
|
||||
"For security, this session has been signed out. Please sign in again.": "Z bezpečnostních důvodů bylo toto přihlášení ukončeno. Přihlašte se prosím znovu.",
|
||||
"and %(count)s others...|other": "a %(count)s další...",
|
||||
"%(widgetName)s widget modified by %(senderName)s": "Uživatel %(senderName)s upravil widget %(widgetName)s",
|
||||
"%(widgetName)s widget removed by %(senderName)s": "Uživatel %(senderName)s odstranil widget %(widgetName)s",
|
||||
"%(widgetName)s widget added by %(senderName)s": "Uživatel %(senderName)s přidal widget %(widgetName)s",
|
||||
"%(widgetName)s widget added by %(senderName)s": "Uživatel %(senderName)s přidal widget %(widgetName)s",
|
||||
"Automatically replace plain text Emoji": "Automaticky nahrazovat textové emoji",
|
||||
"Failed to upload image": "Obrázek se nepodařilo nahrát",
|
||||
"%(senderName)s answered the call.": "Uživatel %(senderName)s přijal hovor.",
|
||||
|
@ -219,7 +219,7 @@
|
|||
"Submit": "Odeslat",
|
||||
"Success": "Úspěch",
|
||||
"The phone number entered looks invalid": "Zadané telefonní číslo se zdá být neplatné",
|
||||
"This email address is already in use": "Tato e-mailová adresa je již požívána",
|
||||
"This email address is already in use": "Tato e-mailová adresa je již používána",
|
||||
"This email address was not found": "Tato e-mailová adresa nebyla nalezena",
|
||||
"This room has no local addresses": "Tato místnost nemá žádné místní adresy",
|
||||
"This room is not recognised.": "Tato místnost nebyla rozpoznána.",
|
||||
|
@ -438,7 +438,7 @@
|
|||
"URL previews are enabled by default for participants in this room.": "Ve výchozím nastavení jsou náhledy URL adres povolené pro členy této místnosti.",
|
||||
"URL previews are disabled by default for participants in this room.": "Ve výchozím nastavení jsou náhledy URL adres zakázané pro členy této místnosti.",
|
||||
"Invalid file%(extra)s": "Neplatný soubor%(extra)s",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Budete přesměrováni na stránku třetí strany k ověření svého účtu pro používání s %(integrationsUrl)s. Chcete pokračovat?",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Budete přesměrováni na stránku třetí strany k ověření svého účtu pro používání s %(integrationsUrl)s. Chcete pokračovat?",
|
||||
"Please check your email to continue registration.": "Pro pokračování v registraci prosím zkontrolujte své e-maily.",
|
||||
"Token incorrect": "Neplatný token",
|
||||
"A text message has been sent to %(msisdn)s": "Na číslo %(msisdn)s byla odeslána textová zpráva",
|
||||
|
@ -557,7 +557,7 @@
|
|||
"Failed to add the following users to the summary of %(groupId)s:": "Do souhrnného seznamu skupiny %(groupId)s se nepodařilo přidat následující uživatele:",
|
||||
"Add a User": "Přidat uživatele",
|
||||
"Failed to remove a user from the summary of %(groupId)s": "Ze souhrnného seznamu skupiny %(groupId)s se nepodařilo odstranit uživatele",
|
||||
"The user '%(displayName)s' could not be removed from the summary.": "Nelze odstranit uživatele '%(displayName)s' ze souhrnného seznamu.",
|
||||
"The user '%(displayName)s' could not be removed from the summary.": "Nelze odstranit uživatele '%(displayName)s' ze souhrnného seznamu.",
|
||||
"Unable to accept invite": "Nelze přijmout pozvání",
|
||||
"Unable to reject invite": "Nelze odmítnout pozvání",
|
||||
"Community Settings": "Nastavení skupiny",
|
||||
|
@ -1332,7 +1332,7 @@
|
|||
"The homeserver may be unavailable or overloaded.": "Domovský server je nedostupný nebo přetížený.",
|
||||
"Add room": "Přidat místnost",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|other": "Máte %(count)s nepřečtených oznámení v předchozí verzi této místnosti.",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|one": "Máte jedno nepřečtené oznámení v předchozí verzi této místnosti.",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|one": "Máte %(count)s nepřečtených oznámení v předchozí verzi této místnosti.",
|
||||
"Your profile": "Váš profil",
|
||||
"Your Matrix account on <underlinedServerName />": "Váš účet Matrix na serveru <underlinedServerName />",
|
||||
"Failed to get autodiscovery configuration from server": "Nepovedlo se automaticky načíst konfiguraci ze serveru",
|
||||
|
@ -1753,7 +1753,7 @@
|
|||
"Review": "Prohlédnout",
|
||||
"This bridge was provisioned by <user />.": "Toto propojení poskytuje <user />.",
|
||||
"This bridge is managed by <user />.": "Toto propojení spravuje <user />.",
|
||||
"Workspace: %(networkName)s": "Workspace: %(networkName)s",
|
||||
"Workspace: %(networkName)s": "Pracovní oblast: %(networkName)s",
|
||||
"Channel: %(channelName)s": "Kanál: %(channelName)s",
|
||||
"Show less": "Skrýt detaily",
|
||||
"Show more": "Více",
|
||||
|
@ -2213,5 +2213,543 @@
|
|||
"Upload completed": "Nahrávání dokončeno",
|
||||
"Cancelled signature upload": "Nahrávání podpisu zrušeno",
|
||||
"Unable to upload": "Nelze nahrát",
|
||||
"Server isn't responding": "Server neodpovídá"
|
||||
"Server isn't responding": "Server neodpovídá",
|
||||
"End": "End",
|
||||
"Space": "Mezerník",
|
||||
"Enter": "Enter",
|
||||
"Esc": "Esc",
|
||||
"Page Down": "Page Down",
|
||||
"Page Up": "Page Up",
|
||||
"Cancel autocomplete": "Zrušit automatické doplňování",
|
||||
"Move autocomplete selection up/down": "Posun nahoru/dolu v automatickém doplňování",
|
||||
"Toggle this dialog": "Zobrazit/skrýt tento dialog",
|
||||
"Activate selected button": "Aktivovat označené tlačítko",
|
||||
"Close dialog or context menu": "Zavřít dialog nebo kontextové menu",
|
||||
"Toggle the top left menu": "Zobrazit/skrýt menu vlevo nahoře",
|
||||
"Scroll up/down in the timeline": "Posunous se nahoru/dolů v historii",
|
||||
"Clear room list filter field": "Smazat filtr místností",
|
||||
"Expand room list section": "Rozbalit seznam místností",
|
||||
"Collapse room list section": "Sbalit seznam místností",
|
||||
"Select room from the room list": "Vybrat místnost v seznamu",
|
||||
"Navigate up/down in the room list": "Posouvat se nahoru/dolů v seznamu místností",
|
||||
"Jump to room search": "Filtrovat místnosti",
|
||||
"Toggle video on/off": "Zapnout nebo vypnout video",
|
||||
"Toggle microphone mute": "Ztlumit nebo zapnout mikrofon",
|
||||
"Jump to start/end of the composer": "Skočit na konec/začátek textového pole",
|
||||
"New line": "Nový řádek",
|
||||
"Toggle Quote": "Citace",
|
||||
"Toggle Italics": "Kurzíva",
|
||||
"Toggle Bold": "Tučné písmo",
|
||||
"Ctrl": "Ctrl",
|
||||
"Shift": "Shift",
|
||||
"Alt Gr": "Alt Gr",
|
||||
"Autocomplete": "Automatické doplňování",
|
||||
"Room List": "Seznam místností",
|
||||
"Calls": "Hovory",
|
||||
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Pro nastavení filtru přetáhněte avatar skupiny do panelu filtrování na levé straně obrazovky. Pak v tomto panelu můžete kdykoliv klepnout na avatar skupiny a uvidíte jen místnosti a lidi z dané skupiny.",
|
||||
"Send feedback": "Odeslat zpětnou vazbu",
|
||||
"Feedback": "Zpětná vazba",
|
||||
"Feedback sent": "Zpětná vazba byla odeslána",
|
||||
"Security & privacy": "Zabezpečení",
|
||||
"All settings": "Nastavení",
|
||||
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Napište jméno nebo emailovou adresu uživatele se kterým chcete začít konverzaci (např. <userId/>).",
|
||||
"Start a new chat": "Založit nový chat",
|
||||
"Which officially provided instance you are using, if any": "Kterou oficiální instanci Riot.im používáte (a jestli vůbec)",
|
||||
"Change the topic of this room": "Změnit téma této místnosti",
|
||||
"%(senderName)s declined the call.": "%(senderName)s odmítl/a hovor.",
|
||||
"(an error occurred)": "(došlo k chybě)",
|
||||
"(their device couldn't start the camera / microphone)": "(zařízení druhé strany nemohlo spustit kameru / mikrofon)",
|
||||
"(connection failed)": "(spojení selhalo)",
|
||||
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 K místnosti nemá přístup žádný server! Místnost už nemůže být používána.",
|
||||
"Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Vloží ( ͡° ͜ʖ ͡°) na začátek zprávy",
|
||||
"Czech Republic": "Česká republika",
|
||||
"Algeria": "Alžírsko",
|
||||
"Albania": "Albánie",
|
||||
"Åland Islands": "Alandy",
|
||||
"Afghanistan": "Afgánistán",
|
||||
"United States": "Spojené Státy",
|
||||
"United Kingdom": "Spojené Království",
|
||||
"This will end the conference for everyone. Continue?": "Ukončíme konferenci pro všechny účastníky. Chcete pokračovat?",
|
||||
"End conference": "Ukončit konferenci",
|
||||
"No other application is using the webcam": "Webkamera není blokována jinou aplikací",
|
||||
"Permission is granted to use the webcam": "Aplikace má k webkameře povolen přístup",
|
||||
"A microphone and webcam are plugged in and set up correctly": "Mikrofon a webkamera jsou zapojeny a správně nastaveny",
|
||||
"Call failed because webcam or microphone could not be accessed. Check that:": "Hovor selhal, protože nešlo použít mikrofon nebo webkameru. Zkontrolujte, že:",
|
||||
"Unable to access webcam / microphone": "Není možné použít webkameru nebo mikrofon",
|
||||
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Hovor selhal, protože nešlo použít mikrofon. Zkontrolujte, že je mikrofon připojen a správně nastaven.",
|
||||
"Unable to access microphone": "Není možné použít mikrofon",
|
||||
"The call was answered on another device.": "Hovor byl přijat na jiném zařízení.",
|
||||
"Answered Elsewhere": "Zodpovězeno jinde",
|
||||
"The call could not be established": "Hovor se nepovedlo navázat",
|
||||
"The other party declined the call.": "Druhá strana hovor odmítla.",
|
||||
"Call Declined": "Hovor odmítnut",
|
||||
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "TIP pro profíky: Pokud nahlásíte chybu, odešlete prosím <debugLogsLink>ladicí protokoly</debugLogsLink>, které nám pomohou problém vypátrat.",
|
||||
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Nejříve si prosím prohlédněte <existingIssuesLink>existující chyby na Githubu</existingIssuesLink>. Žádná shoda? <newIssueLink>Nahlašte novou chybu</newIssueLink>.",
|
||||
"Report a bug": "Nahlásit chybu",
|
||||
"Add widgets, bridges & bots": "Přidat widgety, bridge a boty",
|
||||
"Widgets": "Widgety",
|
||||
"Show Widgets": "Zobrazit widgety",
|
||||
"Hide Widgets": "Skrýt widgety",
|
||||
"Room settings": "Nastavení místnosti",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Pomocí <a>desktopové aplikace</a> zobrazíte všechny šifrované soubory",
|
||||
"Attach files from chat or just drag and drop them anywhere in a room.": "Připojte soubory z chatu nebo je jednoduše přetáhněte kamkoli do místnosti.",
|
||||
"No files visible in this room": "V této místnosti nejsou viditelné žádné soubory",
|
||||
"Show files": "Zobrazit soubory",
|
||||
"%(count)s people|other": "%(count)s lidé",
|
||||
"About": "O",
|
||||
"You’re all caught up": "Vše vyřízeno",
|
||||
"You have no visible notifications in this room.": "V této místnosti nemáte žádná viditelná oznámení.",
|
||||
"Hey you. You're the best!": "Hej ty. Jsi nejlepší!",
|
||||
"Secret storage:": "Bezpečné úložiště:",
|
||||
"Backup key cached:": "Klíč zálohy cachován:",
|
||||
"Backup key stored:": "Klíč zálohy uložen:",
|
||||
"Backup version:": "Verze zálohy:",
|
||||
"Algorithm:": "Algoritmus:",
|
||||
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Tuto možnost můžete povolit, pokud bude místnost použita pouze pro spolupráci s interními týmy na vašem domovském serveru. Toto nelze později změnit.",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Blokovat komukoli, kdo není součástí serveru %(serverName)s, aby se nikdy nepřipojil do této místnosti.",
|
||||
"Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Zálohujte šifrovací klíče s daty vašeho účtu pro případ, že ztratíte přístup k relacím. Vaše klíče budou zabezpečeny jedinečným klíčem pro obnovení.",
|
||||
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Níže můžete spravovat názvy a odhlásit se ze svých relací nebo <a>je ověřit v uživatelském profilu</a>.",
|
||||
"or another cross-signing capable Matrix client": "nebo jiný Matrix klient schopný cross-signing",
|
||||
"Cross-signing is not set up.": "Cross-signing není nastaveno.",
|
||||
"Cross-signing is ready for use.": "Cross-signing je připraveno k použití.",
|
||||
"Create a Group Chat": "Vytvořit skupinový chat",
|
||||
"Send a Direct Message": "Poslat přímou zprávu",
|
||||
"This requires the latest %(brand)s on your other devices:": "To vyžaduje nejnovější %(brand)s na vašich ostatních zařízeních:",
|
||||
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Potvrďte svou identitu ověřením tohoto přihlášení z jedné z vašich dalších relací a udělte mu přístup k šifrovaným zprávám.",
|
||||
"Verify this login": "Ověřte toto přihlášení",
|
||||
"Welcome to %(appName)s": "Vítá vás %(appName)s",
|
||||
"Liberate your communication": "Osvoboďte svou komunikaci",
|
||||
"Navigation": "Navigace",
|
||||
"Use the + to make a new room or explore existing ones below": "Pomocí + vytvořte novou místnost nebo prozkoumejte stávající místnosti",
|
||||
"Secure Backup": "Zabezpečená záloha",
|
||||
"Jump to oldest unread message": "Jít na nejstarší nepřečtenou zprávu",
|
||||
"Upload a file": "Nahrát soubor",
|
||||
"You've reached the maximum number of simultaneous calls.": "Dosáhli jste maximálního počtu souběžných hovorů.",
|
||||
"Too Many Calls": "Přiliš mnoho hovorů",
|
||||
"Community and user menu": "Nabídka komunity a uživatele",
|
||||
"User menu": "Uživatelská nabídka",
|
||||
"Switch theme": "Přepnout téma",
|
||||
"Switch to dark mode": "Přepnout do tmavého režimu",
|
||||
"Switch to light mode": "Přepnout do světlého režimu",
|
||||
"User settings": "Uživatelská nastavení",
|
||||
"Community settings": "Nastavení komunity",
|
||||
"Confirm your recovery passphrase": "Potvrďte vaši frázi pro obnovení",
|
||||
"Repeat your recovery passphrase...": "Opakujte přístupovou frázi pro obnovení...",
|
||||
"Please enter your recovery passphrase a second time to confirm.": "Potvrďte prosím podruhé svou frázi pro obnovení.",
|
||||
"Use a different passphrase?": "Použít jinou frázi?",
|
||||
"Great! This recovery passphrase looks strong enough.": "Skvělé! Tato fráze pro obnovení vypadá dostatečně silně.",
|
||||
"Enter a recovery passphrase": "Zadejte frázi pro obnovení",
|
||||
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s nebo %(usernamePassword)s",
|
||||
"If you've joined lots of rooms, this might take a while": "Pokud jste se připojili ke spoustě místností, může to chvíli trvat",
|
||||
"There was a problem communicating with the homeserver, please try again later.": "Při komunikaci s domovským serverem došlo k potížím, zkuste to prosím později.",
|
||||
"Continue with %(ssoButtons)s": "Pokračovat s %(ssoButtons)s",
|
||||
"Use Recovery Key": "Použít klíč pro obnovu",
|
||||
"Use Recovery Key or Passphrase": "Použít klíč pro obnovu nebo frázi",
|
||||
"Already have an account? <a>Sign in here</a>": "Máte již účet? <a>Přihlašte se zde</a>",
|
||||
"Host account on": "Hostovat účet na",
|
||||
"Signing In...": "Přihlašování...",
|
||||
"Syncing...": "Synchronizuji...",
|
||||
"That username already exists, please try another.": "Toto uživatelské jméno již existuje, zkuste prosím jiné.",
|
||||
"Verify other session": "Ověření jiné relace",
|
||||
"Filter rooms and people": "Filtrovat místnosti a lidi",
|
||||
"Explore rooms in %(communityName)s": "Prozkoumejte místnosti v %(communityName)s",
|
||||
"delete the address.": "smazat adresu.",
|
||||
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Smazat adresu místnosti %(alias)s a odebrat %(name)s z adresáře?",
|
||||
"Self-verification request": "Požadavek na sebeověření",
|
||||
"%(creator)s created this DM.": "%(creator)s vytvořil tuto přímou zprávu.",
|
||||
"You do not have permission to create rooms in this community.": "Nemáte oprávnění k vytváření místností v této komunitě.",
|
||||
"Cannot create rooms in this community": "V této komunitě nelze vytvořit místnosti",
|
||||
"Great, that'll help people know it's you": "Skvělé, to pomůže lidem zjistit, že jste to vy",
|
||||
"Add a photo so people know it's you.": "Přidejte fotku, aby lidé věděli, že jste to vy.",
|
||||
"Explore Public Rooms": "Prozkoumat veřejné místnosti",
|
||||
"Welcome %(name)s": "Vítejte %(name)s",
|
||||
"Now, let's help you get started": "Nyní vám pomůžeme začít",
|
||||
"Effects": "Efekty",
|
||||
"Alt": "Alt",
|
||||
"Approve": "Schválit",
|
||||
"Looks good!": "To vypadá dobře!",
|
||||
"Wrong file type": "Špatný typ souboru",
|
||||
"The server has denied your request.": "Server odmítl váš požadavek.",
|
||||
"The server is offline.": "Server je offline.",
|
||||
"not ready": "nepřipraveno",
|
||||
"Remove messages sent by others": "Odstranit zprávy odeslané ostatními",
|
||||
"Decline All": "Odmítnout vše",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "K prohledávání šifrovaných zpráv použijte <a>aplikaci pro stolní počítače</a>",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Pozvěte někoho pomocí jeho jména, e-mailové adresy, uživatelského jména (například <userId/>) nebo <a>sdílejte tuto místnost</a>.",
|
||||
"Super": "Super",
|
||||
"Toggle right panel": "Zobrazit/skrýt pravý panel",
|
||||
"Add a photo, so people can easily spot your room.": "Přidejte fotografii, aby lidé mohli snadno najít váši místnost.",
|
||||
"%(displayName)s created this room.": "%(displayName)s vytvořil tuto místnost.",
|
||||
"This is the start of <roomName/>.": "Toto je začátek místnosti <roomName/>.",
|
||||
"Topic: %(topic)s ": "Téma: %(topic)s ",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Téma: %(topic)s (<a>upravit</a>)",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Zprávy zde jsou šifrovány end-to-end. Ověřte uživatele %(displayName)s v jeho profilu - klepněte na jeho avatar.",
|
||||
"Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Zadejte frázi zabezpečení, kterou znáte jen vy, k ochraně vašich dat. Z důvodu bezpečnosti byste neměli znovu používat heslo k účtu.",
|
||||
"Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Použijte tajnou frázi, kterou znáte pouze vy, a volitelně uložte bezpečnostní klíč, který použijete pro zálohování.",
|
||||
"Enter a Security Phrase": "Zadání bezpečnostní fráze",
|
||||
"We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Vygenerujeme bezpečnostní klíč, který můžete uložit někde v bezpečí, například ve správci hesel nebo trezoru.",
|
||||
"Generate a Security Key": "Vygenerovat bezpečnostní klíč",
|
||||
"Set up Secure Backup": "Nastavení zabezpečené zálohy",
|
||||
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Chraňte se před ztrátou přístupu k šifrovaným zprávám a datům zálohováním šifrovacích klíčů na serveru.",
|
||||
"Safeguard against losing access to encrypted messages & data": "Zabezpečení proti ztrátě přístupu k šifrovaným zprávám a datům",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "Toto je začátek historie vašich přímých zpráv s uživatelem <displayName />.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "V této konverzaci jste pouze vy dva, dokud někdo z vás nepozve někoho dalšího.",
|
||||
"You created this room.": "Vytvořili jste tuto místnost.",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Přidejte téma</a>, aby lidé věděli, o co jde.",
|
||||
"Invite by email": "Pozvat emailem",
|
||||
"Comment": "Komentář",
|
||||
"Add comment": "Přidat komentář",
|
||||
"Update community": "Aktualizovat komunitu",
|
||||
"Create a room in %(communityName)s": "Vytvořit místnost v %(communityName)s",
|
||||
"Your server requires encryption to be enabled in private rooms.": "Váš server vyžaduje povolení šifrování v soukromých místnostech.",
|
||||
"An image will help people identify your community.": "Obrázek pomůže lidem identifikovat vaši komunitu.",
|
||||
"Invite people to join %(communityName)s": "Pozvat lidi do %(communityName)s",
|
||||
"Send %(count)s invites|one": "Poslat %(count)s pozvánku",
|
||||
"Send %(count)s invites|other": "Poslat %(count)s pozvánek",
|
||||
"People you know on %(brand)s": "Lidé, které znáte z %(brand)s",
|
||||
"Add another email": "Přidat další emailovou adresu",
|
||||
"Show": "Zobrazit",
|
||||
"Reason (optional)": "Důvod (volitelné)",
|
||||
"Add image (optional)": "Přidat obrázek (volitelné)",
|
||||
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Do soukromé místnosti se lze připojit pouze s pozvánkou. Veřejné místnosti lze najít a může se do nich připojit kdokoli.",
|
||||
"Currently indexing: %(currentRoom)s": "Aktuálně se indexuje: %(currentRoom)s",
|
||||
"Secure your backup with a recovery passphrase": "Zabezpečte zálohu pomocí fráze pro obnovení",
|
||||
"%(brand)s Android": "%(brand)s Android",
|
||||
"%(brand)s iOS": "%(brand)s iOS",
|
||||
"%(brand)s Desktop": "%(brand)s Desktop",
|
||||
"%(brand)s Web": "%(brand)s Web",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Pomocí e-mailu můžete být volitelně viditelní pro existující kontakty.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Použijte e-mail nebo telefon, abyste byli volitelně viditelní pro stávající kontakty.",
|
||||
"Sign in with SSO": "Přihlásit pomocí SSO",
|
||||
"Create community": "Vytvořit komunitu",
|
||||
"Add an email to be able to reset your password.": "Přidejte email, abyste mohli obnovit své heslo.",
|
||||
"That phone number doesn't look quite right, please check and try again": "Toto telefonní číslo nevypadá úplně správně, zkontrolujte ho a zkuste to znovu",
|
||||
"Forgot password?": "Zapomenuté heslo?",
|
||||
"Enter phone number": "Zadejte telefonní číslo",
|
||||
"Enter email address": "Zadejte emailovou adresu",
|
||||
"Open the link in the email to continue registration.": "Pro pokračování v registraci, otevřete odkaz v e-mailu.",
|
||||
"A confirmation email has been sent to %(emailAddress)s": "Na adresu %(emailAddress)s byl zaslán potvrzovací e-mail",
|
||||
"Take a picture": "Vyfotit",
|
||||
"Hold": "Podržet",
|
||||
"Resume": "Pokračovat",
|
||||
"Successfully restored %(sessionCount)s keys": "Úspěšně obnoveno %(sessionCount)s klíčů",
|
||||
"Keys restored": "Klíče byly obnoveny",
|
||||
"You're all caught up.": "Vše vyřízeno.",
|
||||
"There was an error updating your community. The server is unable to process your request.": "Při aktualizaci komunity došlo k chybě. Server nemůže zpracovat váš požadavek.",
|
||||
"There are two ways you can provide feedback and help us improve %(brand)s.": "Jsou dva způsoby, jak můžete poskytnout zpětnou vazbu a pomoci nám vylepšit %(brand)s.",
|
||||
"Rate %(brand)s": "Ohodnotit %(brand)s",
|
||||
"You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "V této relaci jste již dříve používali novější verzi %(brand)s. Chcete-li tuto verzi znovu použít s šifrováním, budete se muset odhlásit a znovu přihlásit.",
|
||||
"Homeserver": "Homeserver",
|
||||
"Continue with %(provider)s": "Pokračovat s %(provider)s",
|
||||
"Server Options": "Možnosti serveru",
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Tato verze %(brand)s nepodporuje zobrazení některých šifrovaných souborů",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Tato verze %(brand)s nepodporuje hledání v šifrovaných zprávách",
|
||||
"Information": "Informace",
|
||||
"%(count)s results|one": "%(count)s výsledek",
|
||||
"Explore community rooms": "Prozkoumat místnosti komunity",
|
||||
"Role": "Role",
|
||||
"Madagascar": "Madagaskar",
|
||||
"Macedonia": "Makedonie",
|
||||
"Macau": "Macao",
|
||||
"Luxembourg": "Lucembursko",
|
||||
"Lithuania": "Litva",
|
||||
"Liechtenstein": "Lichtenštejnsko",
|
||||
"Libya": "Libye",
|
||||
"Liberia": "Libérie",
|
||||
"Lesotho": "Lesotho",
|
||||
"Lebanon": "Libanon",
|
||||
"Latvia": "Lotyšsko",
|
||||
"Laos": "Laos",
|
||||
"Kyrgyzstan": "Kyrgyzstán",
|
||||
"Myanmar": "Myanmar",
|
||||
"Mozambique": "Mosambik",
|
||||
"Morocco": "Maroko",
|
||||
"Montserrat": "Montserrat",
|
||||
"Montenegro": "Černá Hora",
|
||||
"Mongolia": "Mongolsko",
|
||||
"Monaco": "Monako",
|
||||
"Moldova": "Moldavsko",
|
||||
"Micronesia": "Mikronésie",
|
||||
"Mexico": "Mexiko",
|
||||
"Mayotte": "Mayotte",
|
||||
"Mauritius": "Mauricius",
|
||||
"Mauritania": "Mauretánie",
|
||||
"Martinique": "Martinik",
|
||||
"Marshall Islands": "Marshallovy ostrovy",
|
||||
"Malta": "Malta",
|
||||
"Mali": "Mali",
|
||||
"Maldives": "Maledivy",
|
||||
"Malaysia": "Malajsie",
|
||||
"Malawi": "Malawi",
|
||||
"Kuwait": "Kuwait",
|
||||
"Kosovo": "Kosovo",
|
||||
"Kiribati": "Kiribati",
|
||||
"Kenya": "Keňa",
|
||||
"Kazakhstan": "Kazachstán",
|
||||
"Congo - Brazzaville": "Kongo - Brazzaville",
|
||||
"Cambodia": "Kambodža",
|
||||
"Burundi": "Burundi",
|
||||
"Burkina Faso": "Burkina Faso",
|
||||
"Bulgaria": "Bulharsko",
|
||||
"Brunei": "Brunej",
|
||||
"British Virgin Islands": "Britské indickooceánské území",
|
||||
"British Indian Ocean Territory": "Britské indickooceánské území",
|
||||
"Brazil": "Brazílie",
|
||||
"Bouvet Island": "Bouvetův ostrov",
|
||||
"Botswana": "Botswana",
|
||||
"Bosnia": "Bosna",
|
||||
"Bolivia": "Bolívie",
|
||||
"Bhutan": "Bhútán",
|
||||
"Bermuda": "Bermudy",
|
||||
"Jordan": "Jordánsko",
|
||||
"Jersey": "Jersey",
|
||||
"Japan": "Japonsko",
|
||||
"Jamaica": "Jamajka",
|
||||
"Italy": "Itálie",
|
||||
"Israel": "Izrael",
|
||||
"Isle of Man": "Ostrov Man",
|
||||
"Ireland": "Irsko",
|
||||
"Iraq": "Irák",
|
||||
"Iran": "Írán",
|
||||
"Indonesia": "Indonésie",
|
||||
"India": "Indie",
|
||||
"Iceland": "Island",
|
||||
"Hungary": "Maďarsko",
|
||||
"Hong Kong": "Hong Kong",
|
||||
"Honduras": "Honduras",
|
||||
"Heard & McDonald Islands": "Heardovy a McDonaldovy ostrovy",
|
||||
"Haiti": "Haiti",
|
||||
"Guyana": "Guyana",
|
||||
"Guinea-Bissau": "Guinea-Bissau",
|
||||
"Guinea": "Guinea",
|
||||
"Guernsey": "Guernsey",
|
||||
"Guatemala": "Guatemala",
|
||||
"Guam": "Guam",
|
||||
"Guadeloupe": "Guadeloupe",
|
||||
"Grenada": "Grenada",
|
||||
"Greenland": "Grónsko",
|
||||
"Greece": "Řecko",
|
||||
"Gibraltar": "Gibraltar",
|
||||
"Ghana": "Ghana",
|
||||
"Germany": "Německo",
|
||||
"Georgia": "Gruzie",
|
||||
"Gambia": "Gambie",
|
||||
"Gabon": "Gabon",
|
||||
"French Southern Territories": "Francouzská jižní území",
|
||||
"French Polynesia": "Francouzská Polynésie",
|
||||
"French Guiana": "Francouzská Guyana",
|
||||
"France": "Francie",
|
||||
"Finland": "Finsko",
|
||||
"Fiji": "Fiji",
|
||||
"Faroe Islands": "Faerské ostrovy",
|
||||
"Falkland Islands": "Falklandy",
|
||||
"Ethiopia": "Etiopie",
|
||||
"Estonia": "Estonsko",
|
||||
"Eritrea": "Eritrea",
|
||||
"Equatorial Guinea": "Rovníková Guinea",
|
||||
"El Salvador": "El Salvador",
|
||||
"Egypt": "Egypt",
|
||||
"Ecuador": "Ekvádor",
|
||||
"Dominican Republic": "Dominikánská republika",
|
||||
"Dominica": "Dominika",
|
||||
"Djibouti": "Džibuti",
|
||||
"Denmark": "Dánsko",
|
||||
"Côte d’Ivoire": "Pobřeží slonoviny",
|
||||
"Cyprus": "Kypr",
|
||||
"Curaçao": "Curaçao",
|
||||
"Cuba": "Kuba",
|
||||
"Croatia": "Chorvatsko",
|
||||
"Costa Rica": "Kostarika",
|
||||
"Cook Islands": "Cookovy ostrovy",
|
||||
"Congo - Kinshasa": "Kongo - Brazzaville",
|
||||
"Comoros": "Komory",
|
||||
"Colombia": "Kolumbie",
|
||||
"Cocos (Keeling) Islands": "Kokosové (Keelingovy) ostrovy",
|
||||
"Christmas Island": "Vánoční ostrov",
|
||||
"China": "Čína",
|
||||
"Chile": "Chile",
|
||||
"Chad": "Čad",
|
||||
"Central African Republic": "Středoafrická republika",
|
||||
"Cayman Islands": "Kajmanské ostrovy",
|
||||
"Caribbean Netherlands": "Karibské Nizozemsko",
|
||||
"Cape Verde": "Kapverdy",
|
||||
"Canada": "Kanada",
|
||||
"Cameroon": "Kamerun",
|
||||
"Benin": "Benin",
|
||||
"Belize": "Belize",
|
||||
"Belgium": "Belgie",
|
||||
"Belarus": "Bělorusko",
|
||||
"Bangladesh": "Bangladéš",
|
||||
"Barbados": "Barbados",
|
||||
"Bahrain": "Bahrain",
|
||||
"Bahamas": "Bahamy",
|
||||
"Azerbaijan": "Ázerbajdžán",
|
||||
"Austria": "Rakousko",
|
||||
"Australia": "Austrálie",
|
||||
"Aruba": "Aruba",
|
||||
"Armenia": "Arménie",
|
||||
"Argentina": "Argentina",
|
||||
"Antigua & Barbuda": "Antigua a Barbuda",
|
||||
"Antarctica": "Antarktida",
|
||||
"Anguilla": "Anguilla",
|
||||
"Angola": "Angola",
|
||||
"Andorra": "Andorra",
|
||||
"American Samoa": "Americká Samoa",
|
||||
"Room Info": "Informace o místnosti",
|
||||
"Enable desktop notifications": "Povolit oznámení na ploše",
|
||||
"Invalid Recovery Key": "Neplatný klíč pro obnovení",
|
||||
"Security Key": "Bezpečnostní klíč",
|
||||
"Use your Security Key to continue.": "Pokračujte pomocí bezpečnostního klíče.",
|
||||
"If they don't match, the security of your communication may be compromised.": "Pokud se neshodují, bezpečnost vaší komunikace může být kompromitována.",
|
||||
"Confirm this user's session by comparing the following with their User Settings:": "Potvrďte relaci tohoto uživatele porovnáním následujícího s jeho uživatelským nastavením:",
|
||||
"Confirm Security Phrase": "Potvrďte bezpečnostní frázi",
|
||||
"Privacy": "Soukromí",
|
||||
"Click the button below to confirm setting up encryption.": "Kliknutím na tlačítko níže potvrďte nastavení šifrování.",
|
||||
"Confirm encryption setup": "Potvrďte nastavení šifrování",
|
||||
"Return to call": "Návrat do hovoru",
|
||||
"Unable to set up keys": "Nepovedlo se nastavit klíče",
|
||||
"Save your Security Key": "Uložte svůj bezpečnostní klíč",
|
||||
"We call the places where you can host your account ‘homeservers’.": "Místům, kde můžete hostovat svůj účet, říkáme ‘domovské servery’.",
|
||||
"About homeservers": "O domovských serverech",
|
||||
"Learn more": "Zjistit více",
|
||||
"Use your preferred Matrix homeserver if you have one, or host your own.": "Použijte svůj preferovaný domovský server Matrix, pokud ho máte, nebo hostujte svůj vlastní.",
|
||||
"Other homeserver": "Jiný domovský server",
|
||||
"Sign into your homeserver": "Přihlaste se do svého domovského serveru",
|
||||
"Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org je největší veřejný domovský server na světě, pro mnoho lidí je dobrým místem.",
|
||||
"not found in storage": "nebylo nalezeno v úložišti",
|
||||
"ready": "připraven",
|
||||
"May include members not in %(communityName)s": "Může zahrnovat členy, kteří nejsou v %(communityName)s",
|
||||
"Specify a homeserver": "Zadejte domovský server",
|
||||
"Invalid URL": "Neplatné URL",
|
||||
"Unable to validate homeserver": "Nelze ověřit domovský server",
|
||||
"New? <a>Create account</a>": "Jste zde nový? <a>Vytvořte si účet</a>",
|
||||
"Navigate recent messages to edit": "Procházet poslední zprávy k úpravám",
|
||||
"Navigate composer history": "Procházet historii editoru",
|
||||
"Cancel replying to a message": "Zrušení odpovědi na zprávu",
|
||||
"Don't miss a reply": "Nezmeškejte odpovědět",
|
||||
"Unknown App": "Neznámá aplikace",
|
||||
"%(name)s paused": "%(name)s pozastaven",
|
||||
"Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.": "Zálohu nebylo možné dešifrovat pomocí tohoto klíče pro obnovení: ověřte, zda jste zadali správný klíč pro obnovení.",
|
||||
"Move right": "Posunout doprava",
|
||||
"Move left": "Posunout doleva",
|
||||
"Go to Home View": "Přejít na domovské zobrazení",
|
||||
"Dismiss read marker and jump to bottom": "Zavřít značku přečtených zpráv a skočit dolů",
|
||||
"Previous/next room or DM": "Předchozí/další místnost nebo přímá zpráva",
|
||||
"Previous/next unread room or DM": "Předchozí/další nepřečtená místnost nebo přímá zpráva",
|
||||
"Not encrypted": "Není šifrováno",
|
||||
"New here? <a>Create an account</a>": "Jste zde nový? <a>Vytvořte si účet</a>",
|
||||
"Got an account? <a>Sign in</a>": "Máte již účet? <a>Přihlásit se</a>",
|
||||
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Toto je nepozve do %(communityName)s. Chcete-li někoho pozvat na %(communityName)s, klikněte <a>sem</a>",
|
||||
"Approve widget permissions": "Schválit oprávnění widgetu",
|
||||
"Enter name": "Zadejte jméno",
|
||||
"Ignored attempt to disable encryption": "Ignorovaný pokus o deaktivaci šifrování",
|
||||
"Show chat effects": "Zobrazit efekty chatu",
|
||||
"%(count)s people|one": "%(count)s člověk",
|
||||
"Takes the call in the current room off hold": "Zruší podržení hovoru v aktuální místnosti",
|
||||
"Places the call in the current room on hold": "Podrží hovor v aktuální místnosti",
|
||||
"Zimbabwe": "Zimbabwe",
|
||||
"Zambia": "Zambie",
|
||||
"Yemen": "Jemen",
|
||||
"Western Sahara": "Západní Sahara",
|
||||
"Wallis & Futuna": "Wallis a Futuna",
|
||||
"Vietnam": "Vietnam",
|
||||
"Venezuela": "Venezuela",
|
||||
"Vatican City": "Vatikán",
|
||||
"Vanuatu": "Vanuatu",
|
||||
"Uzbekistan": "Uzbekistán",
|
||||
"Uruguay": "Uruguay",
|
||||
"United Arab Emirates": "Spojené arabské emiráty",
|
||||
"Ukraine": "Ukrajina",
|
||||
"Uganda": "Uganda",
|
||||
"U.S. Virgin Islands": "Panenské ostrovy",
|
||||
"Tuvalu": "Tuvalu",
|
||||
"Turks & Caicos Islands": "Ostrovy Turks a Caicos",
|
||||
"Turkmenistan": "Turkmenistán",
|
||||
"Turkey": "Turecko",
|
||||
"Tunisia": "Tunisko",
|
||||
"Trinidad & Tobago": "Trinidad a Tobago",
|
||||
"Tonga": "Tonga",
|
||||
"Tokelau": "Tokelau",
|
||||
"Togo": "Togo",
|
||||
"Timor-Leste": "Východní Timor",
|
||||
"Thailand": "Thajsko",
|
||||
"Tanzania": "Tanzánie",
|
||||
"Tajikistan": "Tádžikistán",
|
||||
"Taiwan": "Taiwan",
|
||||
"São Tomé & Príncipe": "Svatý Tomáš a Princův ostrov",
|
||||
"Syria": "Sýrie",
|
||||
"Switzerland": "Švýcarsko",
|
||||
"Sweden": "Švédsko",
|
||||
"Swaziland": "Svazijsko",
|
||||
"Svalbard & Jan Mayen": "Špicberky a Jan Mayen",
|
||||
"Suriname": "Surinam",
|
||||
"Sudan": "Súdán",
|
||||
"St. Vincent & Grenadines": "Svatý Vincenc a Grenadiny",
|
||||
"St. Pierre & Miquelon": "Saint Pierre a Miquelon",
|
||||
"St. Martin": "Svatý Martin",
|
||||
"St. Lucia": "Svatá Lucie",
|
||||
"St. Kitts & Nevis": "Svatý Kryštof a Nevis",
|
||||
"St. Helena": "Svatá Helena",
|
||||
"St. Barthélemy": "Svatý Bartoloměj",
|
||||
"Sri Lanka": "Srí Lanka",
|
||||
"Spain": "Španělsko",
|
||||
"South Sudan": "Jižní Súdán",
|
||||
"South Korea": "Jižní Korea",
|
||||
"South Georgia & South Sandwich Islands": "Jižní Georgie a Jižní Sandwichovy ostrovy",
|
||||
"South Africa": "Jižní Afrika",
|
||||
"Somalia": "Somálsko",
|
||||
"Solomon Islands": "Šalomounovy ostrovy",
|
||||
"Slovenia": "Slovinsko",
|
||||
"Slovakia": "Slovensko",
|
||||
"Sint Maarten": "Sint Maarten",
|
||||
"Singapore": "Singapur",
|
||||
"Sierra Leone": "Sierra Leone",
|
||||
"Seychelles": "Seychely",
|
||||
"Serbia": "Srbsko",
|
||||
"Senegal": "Senegal",
|
||||
"Saudi Arabia": "Saudská arábie",
|
||||
"San Marino": "San Marino",
|
||||
"Samoa": "Samoa",
|
||||
"Réunion": "Réunion",
|
||||
"Rwanda": "Rwanda",
|
||||
"Russia": "Rusko",
|
||||
"Romania": "Rumunsko",
|
||||
"Qatar": "Katar",
|
||||
"Puerto Rico": "Portoriko",
|
||||
"Portugal": "Portugalsko",
|
||||
"Poland": "Polsko",
|
||||
"Pitcairn Islands": "Pitcairnovy ostrovy",
|
||||
"Philippines": "Filipíny",
|
||||
"Peru": "Peru",
|
||||
"Paraguay": "Paraguay",
|
||||
"Papua New Guinea": "Papua-Nová Guinea",
|
||||
"Panama": "Panama",
|
||||
"Palestine": "Palestina",
|
||||
"Palau": "Palau",
|
||||
"Pakistan": "Pákistán",
|
||||
"Oman": "Omán",
|
||||
"Norway": "Norsko",
|
||||
"Northern Mariana Islands": "Severní Mariany",
|
||||
"North Korea": "Severní Korea",
|
||||
"Norfolk Island": "Ostrov Norfolk",
|
||||
"Niue": "Niue",
|
||||
"Nigeria": "Nigérie",
|
||||
"Niger": "Niger",
|
||||
"Nicaragua": "Nikaragua",
|
||||
"New Zealand": "Nový Zéland",
|
||||
"New Caledonia": "Nová Kaledonie",
|
||||
"Netherlands": "Holandsko",
|
||||
"Nepal": "Nepál",
|
||||
"Nauru": "Nauru",
|
||||
"Namibia": "Namibie",
|
||||
"Security Phrase": "Bezpečnostní fráze",
|
||||
"Wrong Recovery Key": "Nesprávný klíč pro obnovení",
|
||||
"Fetching keys from server...": "Načítání klíčů ze serveru ...",
|
||||
"Use Command + Enter to send a message": "K odeslání zprávy použijte Command + Enter",
|
||||
"Use Ctrl + Enter to send a message": "K odeslání zprávy použijte Ctrl + Enter",
|
||||
"Decide where your account is hosted": "Rozhodněte, kde je váš účet hostován",
|
||||
"Unable to query secret storage status": "Nelze zjistit stav úložiště klíčů",
|
||||
"Update %(brand)s": "Aktualizovat %(brand)s",
|
||||
"You can also set up Secure Backup & manage your keys in Settings.": "Zabezpečené zálohování a správu klíčů můžete také nastavit v Nastavení.",
|
||||
"Set a Security Phrase": "Nastavit bezpečnostní frázi"
|
||||
}
|
||||
|
|
|
@ -301,7 +301,7 @@
|
|||
"Drop file here to upload": "Datei hier loslassen zum hochladen",
|
||||
"Idle": "Untätig",
|
||||
"Ongoing conference call%(supportedText)s.": "Laufendes Konferenzgespräch%(supportedText)s.",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Du wirst jetzt auf die Website eines Drittanbieters weitergeleitet, damit du dein Benutzerkonto für die Verwendung von %(integrationsUrl)s authentifizieren kannst. Möchtest du fortfahren?",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Du wirst jetzt auf die Website eines Drittanbieters weitergeleitet, damit du dein Benutzerkonto für die Verwendung von %(integrationsUrl)s authentifizieren kannst. Möchtest du fortfahren?",
|
||||
"Start automatically after system login": "Nach System-Login automatisch starten",
|
||||
"Jump to first unread message.": "Zur ersten ungelesenen Nachricht springen.",
|
||||
"Options": "Optionen",
|
||||
|
@ -604,9 +604,9 @@
|
|||
"Flair": "Abzeichen",
|
||||
"Showing flair for these communities:": "Abzeichen für diese Communities zeigen:",
|
||||
"This room is not showing flair for any communities": "Dieser Raum zeigt für keine Communities die Abzeichen an",
|
||||
"Something went wrong when trying to get your communities.": "Beim Laden deiner Communites ist etwas schief gelaufen.",
|
||||
"Something went wrong when trying to get your communities.": "Beim Laden deiner Communities ist etwas schief gelaufen.",
|
||||
"Display your community flair in rooms configured to show it.": "Zeige deinen Community-Flair in den Räumen, die es erlauben.",
|
||||
"This homeserver doesn't offer any login flows which are supported by this client.": "Dieser Heimserver verfügt über keinen, von diesem Client unterstütztes Anmeldeverfahren.",
|
||||
"This homeserver doesn't offer any login flows which are supported by this client.": "Dieser Heimserver verfügt über kein von diesem Client unterstütztes Anmeldeverfahren.",
|
||||
"Call Failed": "Anruf fehlgeschlagen",
|
||||
"Send": "Senden",
|
||||
"collapse": "Verbergen",
|
||||
|
@ -711,7 +711,7 @@
|
|||
"Enter keywords separated by a comma:": "Schlüsselwörter kommagetrennt eingeben:",
|
||||
"Forward Message": "Nachricht weiterleiten",
|
||||
"You have successfully set a password and an email address!": "Du hast erfolgreich ein Passwort und eine E-Mail-Adresse gesetzt!",
|
||||
"Remove %(name)s from the directory?": "Soll der Raum %(name)s aus dem Verzeichnis entfernt werden?",
|
||||
"Remove %(name)s from the directory?": "Soll der Raum %(name)s aus dem Verzeichnis entfernt werden?",
|
||||
"%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s nutzt zahlreiche fortgeschrittene Browser-Funktionen, die teilweise in deinem aktuell verwendeten Browser noch nicht verfügbar sind oder sich noch im experimentellen Status befinden.",
|
||||
"Developer Tools": "Entwicklerwerkzeuge",
|
||||
"Preparing to send logs": "Senden von Logs wird vorbereitet",
|
||||
|
@ -1099,7 +1099,7 @@
|
|||
"Anchor": "Anker",
|
||||
"Headphones": "Kopfhörer",
|
||||
"Folder": "Ordner",
|
||||
"Pin": "Stecknadel",
|
||||
"Pin": "Anheften",
|
||||
"Timeline": "Chatverlauf",
|
||||
"Autocomplete delay (ms)": "Verzögerung zur Autovervollständigung (ms)",
|
||||
"Roles & Permissions": "Rollen & Berechtigungen",
|
||||
|
@ -2056,7 +2056,7 @@
|
|||
"Your new session is now verified. Other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Andere Benutzer sehen sie als vertrauenswürdig an.",
|
||||
"well formed": "wohlgeformt",
|
||||
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Wenn du <server /> nicht verwenden willst, um Kontakte zu finden und von anderen gefunden zu werden, trage unten einen anderen Identitätsserver ein.",
|
||||
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Wenn du einen sicherheitsrelevaten Fehler melden möchtest, lies bitte die Matrix.org <a>Security Disclosure Policy</a>.",
|
||||
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Wenn du einen sicherheitsrelevanten Fehler melden möchtest, lies bitte die Matrix.org <a>Security Disclosure Policy</a>.",
|
||||
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Beim Ändern der Anforderungen für Benutzerrechte ist ein Fehler aufgetreten. Stelle sicher dass du die nötigen Berechtigungen besitzt und versuche es erneut.",
|
||||
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Beim Ändern der Benutzerrechte ist ein Fehler aufgetreten. Stelle sicher dass du die nötigen Berechtigungen besitzt und versuche es erneut.",
|
||||
"Unable to share email address": "E-Mail Adresse konnte nicht geteilt werden",
|
||||
|
@ -2187,8 +2187,8 @@
|
|||
"Font size": "Schriftgröße",
|
||||
"IRC display name width": "Breite des IRC Anzeigenamens",
|
||||
"Size must be a number": "Größe muss eine Zahl sein",
|
||||
"Custom font size can only be between %(min)s pt and %(max)s pt": "Eigene Schriftgröße kann nur eine Zahl zwischen %(min)s pt und %(max)s pt sein",
|
||||
"Use between %(min)s pt and %(max)s pt": "Verwende eine Zahl zwischen %(min)s pt und %(max)s pt",
|
||||
"Custom font size can only be between %(min)s pt and %(max)s pt": "Eigene Schriftgröße kann nur eine Zahl zwischen %(min)s pt und %(max)s pt sein",
|
||||
"Use between %(min)s pt and %(max)s pt": "Verwende eine Zahl zwischen %(min)s pt und %(max)s pt",
|
||||
"Appearance": "Erscheinungsbild",
|
||||
"Create room": "Raum erstellen",
|
||||
"Jump to oldest unread message": "Zur ältesten ungelesenen Nachricht springen",
|
||||
|
@ -2552,5 +2552,403 @@
|
|||
"Report a bug": "Einen Fehler melden",
|
||||
"Add comment": "Kommentar hinzufügen",
|
||||
"Rate %(brand)s": "%(brand)s bewerten",
|
||||
"Feedback sent": "Feedback gesendet"
|
||||
"Feedback sent": "Feedback gesendet",
|
||||
"Takes the call in the current room off hold": "Beendet das Halten des Anrufs",
|
||||
"Places the call in the current room on hold": "Den aktuellen Anruf halten",
|
||||
"Uzbekistan": "Usbekistan",
|
||||
"Send stickers into this room": "Stickers in diesen Raum senden",
|
||||
"Send stickers into your active room": "Stickers in deinen aktiven Raum senden",
|
||||
"Change which room you're viewing": "Ändern welchen Raum du siehst",
|
||||
"Change the topic of this room": "Das Thema von diesem Raum ändern",
|
||||
"See when the topic changes in this room": "Sehen wenn sich das Thema in diesem Raum ändert",
|
||||
"Change the topic of your active room": "Das Thema von deinem aktiven Raum ändern",
|
||||
"See when the topic changes in your active room": "Sehen wenn sich das Thema in deinem aktiven Raum ändert",
|
||||
"Change the name of this room": "Name von diesem Raum ändern",
|
||||
"See when the name changes in this room": "Sehen wenn sich der Name in diesem Raum ändert",
|
||||
"Change the name of your active room": "Den Namen deines aktiven Raums ändern",
|
||||
"See when the name changes in your active room": "Sehen wenn der Name sich in deinem aktiven Raum ändert",
|
||||
"Change the avatar of this room": "Avatar von diesem Raum ändern",
|
||||
"See when the avatar changes in this room": "Sehen wenn der Avatar sich in diesem Raum ändert",
|
||||
"Change the avatar of your active room": "Den Avatar deines aktiven Raums ändern",
|
||||
"See when the avatar changes in your active room": "Sehen wenn ein Avatar in deinem aktiven Raum geändert wird",
|
||||
"Send stickers to this room as you": "Einen Sticker in diesen Raum als du senden",
|
||||
"See when a sticker is posted in this room": "Sehe wenn ein Sticker in diesen Raum gesendet wird",
|
||||
"Send stickers to your active room as you": "Einen Sticker als du in deinen aktiven Raum senden",
|
||||
"See when anyone posts a sticker to your active room": "Sehen wenn jemand einen Sticker in deinen aktiven Raum sendet",
|
||||
"with an empty state key": "mit einem leeren Zustandsschlüssel",
|
||||
"with state key %(stateKey)s": "mit Zustandsschlüssel %(stateKey)s",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "<b>%(eventType)s</b>-Events als du in diesem Raum senden",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "In diesem Raum gesendete <b>%(eventType)s</b>-Events anzeigen",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "<b>%(eventType)s</b>-Events als du in deinem aktiven Raum senden",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "In deinem aktiven Raum gesendete <b>%(eventType)s</b>-Events anzeigen",
|
||||
"The <b>%(capability)s</b> capability": "Die <b>%(capability)s</b> Fähigkeit",
|
||||
"Send messages as you in this room": "Nachrichten als du in diesem Raum senden",
|
||||
"Send messages as you in your active room": "Eine Nachricht als du in deinem aktiven Raum senden",
|
||||
"See messages posted to this room": "In diesem Raum gesendete Nachrichten anzeigen",
|
||||
"See messages posted to your active room": "In deinem aktiven Raum gesendete Nachrichten anzeigen",
|
||||
"Send text messages as you in this room": "Textnachrichten als du in diesem Raum senden",
|
||||
"Send text messages as you in your active room": "Textnachrichten als du in deinem aktiven Raum senden",
|
||||
"See text messages posted to this room": "In diesem Raum gesendete Textnachrichten anzeigen",
|
||||
"See text messages posted to your active room": "In deinem aktiven Raum gesendete Textnachrichten anzeigen",
|
||||
"Send emotes as you in this room": "Emojis als du in diesem Raum senden",
|
||||
"Send emotes as you in your active room": "Emojis als du in deinem aktiven Raum senden",
|
||||
"See emotes posted to this room": "In diesem Raum gesendete Emojis anzeigen",
|
||||
"See emotes posted to your active room": "In deinem aktiven Raum gesendete Emojis anzeigen",
|
||||
"See videos posted to your active room": "In deinem aktiven Raum gesendete Videos anzeigen",
|
||||
"See videos posted to this room": "In diesem Raum gesendete Videos anzeigen",
|
||||
"Send images as you in this room": "Bilder als du in diesem Raum senden",
|
||||
"Send images as you in your active room": "Bilder als du in deinem aktiven Raum senden",
|
||||
"See images posted to this room": "In diesem Raum gesendete Bilder anzeigen",
|
||||
"See images posted to your active room": "In deinem aktiven Raum gesendete Bilder anzeigen",
|
||||
"Send videos as you in this room": "Videos als du in diesem Raum senden",
|
||||
"Send videos as you in your active room": "Videos als du in deinem aktiven Raum senden",
|
||||
"Send general files as you in this room": "Allgemeine Dateien als du in diesem Raum senden",
|
||||
"Send general files as you in your active room": "Allgemeine Dateien als du in deinem aktiven Raum senden",
|
||||
"See general files posted to your active room": "Allgemeine in deinem aktiven Raum gesendete Dateien anzeigen",
|
||||
"See general files posted to this room": "Allgemeine in diesem Raum gesendete Dateien anzeigen",
|
||||
"Send <b>%(msgtype)s</b> messages as you in this room": "Sende <b>%(msgtype)s</b> Nachrichten als du in diesem Raum",
|
||||
"Send <b>%(msgtype)s</b> messages as you in your active room": "Sende <b>%(msgtype)s</b> Nachrichten als du in deinem aktiven Raum",
|
||||
"See <b>%(msgtype)s</b> messages posted to this room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in diesem Raum gesendet worden sind",
|
||||
"See <b>%(msgtype)s</b> messages posted to your active room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in deinem aktiven Raum gesendet worden sind",
|
||||
"Don't miss a reply": "Verpasse keine Antwort",
|
||||
"Enable desktop notifications": "Aktiviere Desktopbenachrichtigungen",
|
||||
"Update %(brand)s": "Aktualisiere %(brand)s",
|
||||
"New version of %(brand)s is available": "Neue Version von %(brand)s verfügbar",
|
||||
"You ended the call": "Du hast den Anruf beendet",
|
||||
"%(senderName)s ended the call": "%(senderName)s hat den Anruf beendet",
|
||||
"Use Command + Enter to send a message": "Benutze Betriebssystemtaste + Enter um eine Nachricht zu senden",
|
||||
"Use Ctrl + Enter to send a message": "Benutze Strg + Enter um eine Nachricht zu senden",
|
||||
"Call Paused": "Anruf pausiert",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern um sie in Suchergebnissen finden zu können, benötigt %(size)s um die Nachrichten von den Räumen %(rooms)s zu speichern.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern um sie in Suchergebnissen finden zu können, benötigt %(size)s um die Nachrichten vom Raum %(rooms)s zu speichern.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Nur ihr zwei seid in dieser Konversation, außer einer von euch lädt jemanden neues ein.",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "Dies ist der Beginn deiner Direktnachrichtenhistorie mit <displayName/>.",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Thema: %(topic)s (<a>ändern</a>)",
|
||||
"Topic: %(topic)s ": "Thema: %(topic)s ",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Füge ein Thema hinzu</a> um Personen zu verdeutlichen um was es in ihm geht.",
|
||||
"You created this room.": "Du hast diesen Raum erstellt.",
|
||||
"%(displayName)s created this room.": "%(displayName)s erstellte diesen Raum.",
|
||||
"Add a photo, so people can easily spot your room.": "Füge ein Foto hinzu, sodass Personen deinen Raum einfach finden können.",
|
||||
"This is the start of <roomName/>.": "Dies ist der Beginn von <roomName/>.",
|
||||
"Start a new chat": "Starte einen neuen Chat",
|
||||
"Role": "Rolle",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Nachrichten hier sind Ende-zu-Ende-verschlüsselt. Verifiziere %(displayName)s im deren Profil - klicke auf deren Avatar.",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Wenn Personen beitreten, kannst du sie in ihrem Profil verifizieren, klicke hierfür auf deren Avatar.",
|
||||
"Tell us below how you feel about %(brand)s so far.": "Erzähle uns wie %(brand)s dir soweit gefällt.",
|
||||
"Please go into as much detail as you like, so we can track down the problem.": "Bitte nenne so viele Details wie du möchtest, sodass wir das Problem finden können.",
|
||||
"Comment": "Kommentar",
|
||||
"There are two ways you can provide feedback and help us improve %(brand)s.": "Es gibt zwei Wege wie du Feedback geben kannst und uns helfen kannst %(brand)s zu verbessern.",
|
||||
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Bitte wirf einen Blick auf <existingIssuesLink>existierende Bugs auf Github</existingIssuesLink>. Keinen gefunden? <newIssueLink>Erstelle einen neuen</newIssueLink>.",
|
||||
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIPP: Wenn du einen Bug meldest, füge bitte <debugLogsLink>Debug-Logs</debugLogsLink> hinzu um uns zu helfen das Problem zu finden.",
|
||||
"Invite by email": "Via Email einladen",
|
||||
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Beginne eine Konversation mit jemanden unter Benutzung des Namens, Email-Addresse oder Benutzername (siehe <userId/>).",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Lade jemanden unter Benutzung seines Namens, E-Mailaddresse oder Benutzername (siehe <userId/>) ein, oder <a>teile diesen Raum</a>.",
|
||||
"Approve widget permissions": "Rechte für das Widget genehmigen",
|
||||
"This widget would like to:": "Dieses Widget würde gerne:",
|
||||
"Approve": "Zustimmen",
|
||||
"Decline All": "Alles ablehnen",
|
||||
"Go to Home View": "Zur Startseite gehen",
|
||||
"Filter rooms and people": "Räume und Personen filtern",
|
||||
"%(creator)s created this DM.": "%(creator)s hat diese DM erstellt.",
|
||||
"Now, let's help you get started": "Nun, lassen Sie uns Ihnen den Einstieg erleichtern",
|
||||
"Welcome %(name)s": "Willkommen %(name)s",
|
||||
"Add a photo so people know it's you.": "Fügen Sie ein Foto hinzu, damit die Leute wissen, dass Sie es sind.",
|
||||
"Great, that'll help people know it's you": "Großartig, das wird den Leuten helfen, zu wissen, dass Sie es sind",
|
||||
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML für die Seite Ihrer Community</h1>\n<p>\n Verwenden Sie die ausführliche Beschreibung, um die Community neuen Mitgliedern vorzustellen,\n oder teilen Sie einige wichtige Links <a href=\"foo\">links</a>\n</p>\n<p>\n Sie können sogar Bilder mit Matrix-URLs hinzufügen <img src=\"mxc://url\" />\n</p>\n",
|
||||
"Enter phone number": "Telefonnummer eingeben",
|
||||
"Enter email address": "E-Mail-Adresse eingeben",
|
||||
"Open the link in the email to continue registration.": "Öffnen Sie den Link in der E-Mail, um mit der Registrierung fortzufahren.",
|
||||
"A confirmation email has been sent to %(emailAddress)s": "Eine Bestätigungs-E-Mail wurde an %(emailAddress)s gesendet",
|
||||
"Use the + to make a new room or explore existing ones below": "Verwenden Sie das +, um einen neuen Raum zu erstellen oder unten einen bestehenden zu erkunden",
|
||||
"Return to call": "Zurück zum Anruf",
|
||||
"Fill Screen": "Bildschirm ausfüllen",
|
||||
"Voice Call": "Sprachanruf",
|
||||
"Video Call": "Videoanruf",
|
||||
"Remain on your screen while running": "Bleiben Sie auf Ihrem Bildschirm während der Ausführung von",
|
||||
"Remain on your screen when viewing another room, when running": "Bleiben Sie auf Ihrem Bildschirm, während Sie einen anderen Raum betrachten, wenn Sie ausführen",
|
||||
"Zimbabwe": "Simbabwe",
|
||||
"Zambia": "Sambia",
|
||||
"Yemen": "Jemen",
|
||||
"Western Sahara": "Westsahara",
|
||||
"Wallis & Futuna": "Wallis und Futuna",
|
||||
"Vietnam": "Vietnam",
|
||||
"Venezuela": "Venezuela",
|
||||
"Vatican City": "Vatikanstadt",
|
||||
"Vanuatu": "Vanuatu",
|
||||
"Uruguay": "Uruguay",
|
||||
"United Arab Emirates": "Vereinigte Arabische Emirate",
|
||||
"Ukraine": "Ukraine",
|
||||
"Uganda": "Uganda",
|
||||
"U.S. Virgin Islands": "Amerikanische Jungferninseln",
|
||||
"Tuvalu": "Tuvalu",
|
||||
"Turks & Caicos Islands": "Turks- und Caicosinseln",
|
||||
"Turkmenistan": "Turkmenistan",
|
||||
"Turkey": "Türkei",
|
||||
"Tunisia": "Tunesien",
|
||||
"Trinidad & Tobago": "Trinidad und Tobago",
|
||||
"Tonga": "Tonga",
|
||||
"Tokelau": "Tokelau",
|
||||
"Togo": "Togo",
|
||||
"Timor-Leste": "Timor-Leste",
|
||||
"Thailand": "Thailand",
|
||||
"Tanzania": "Tansania",
|
||||
"Tajikistan": "Tadschikistan",
|
||||
"Taiwan": "Taiwan",
|
||||
"São Tomé & Príncipe": "São Tomé und Príncipe",
|
||||
"Syria": "Syrien",
|
||||
"Switzerland": "Schweiz",
|
||||
"Sweden": "Schweden",
|
||||
"Swaziland": "Swasiland",
|
||||
"Svalbard & Jan Mayen": "Spitzbergen & Jan Mayen",
|
||||
"Suriname": "Surinam",
|
||||
"Sudan": "Sudan",
|
||||
"St. Vincent & Grenadines": "St. Vincent und die Grenadinen",
|
||||
"St. Pierre & Miquelon": "St. Pierre & Miquelon",
|
||||
"St. Martin": "St. Martin",
|
||||
"St. Lucia": "St. Lucia",
|
||||
"St. Kitts & Nevis": "St. Kitts & Nevis",
|
||||
"St. Helena": "St. Helena",
|
||||
"St. Barthélemy": "St. Barthélemy",
|
||||
"Sri Lanka": "Sri Lanka",
|
||||
"Spain": "Spanien",
|
||||
"South Sudan": "Südsudan",
|
||||
"South Korea": "Südkorea",
|
||||
"South Georgia & South Sandwich Islands": "Südgeorgien & Südliche Sandwichinseln",
|
||||
"South Africa": "Südafrika",
|
||||
"Somalia": "Somalia",
|
||||
"Solomon Islands": "Salomonen",
|
||||
"Slovenia": "Slowenien",
|
||||
"Slovakia": "Slowakei",
|
||||
"Sint Maarten": "St. Martin",
|
||||
"Singapore": "Singapur",
|
||||
"Sierra Leone": "Sierra Leone",
|
||||
"Seychelles": "Seychellen",
|
||||
"Serbia": "Serbien",
|
||||
"Senegal": "Senegal",
|
||||
"Saudi Arabia": "Saudi-Arabien",
|
||||
"San Marino": "San Marino",
|
||||
"Samoa": "Samoa",
|
||||
"Réunion": "Réunion",
|
||||
"Rwanda": "Ruanda",
|
||||
"Russia": "Russland",
|
||||
"Romania": "Rumänien",
|
||||
"Qatar": "Katar",
|
||||
"Puerto Rico": "Puerto Rico",
|
||||
"Portugal": "Portugal",
|
||||
"Poland": "Polen",
|
||||
"Pitcairn Islands": "Pitcairninseln",
|
||||
"Philippines": "Philippinen",
|
||||
"Peru": "Peru",
|
||||
"Paraguay": "Paraguay",
|
||||
"Papua New Guinea": "Papua-Neuguinea",
|
||||
"Panama": "Panama",
|
||||
"Palestine": "Palästina",
|
||||
"Palau": "Palau",
|
||||
"Pakistan": "Pakistan",
|
||||
"Oman": "Oman",
|
||||
"Norway": "Norwegen",
|
||||
"Northern Mariana Islands": "Nördliche Marianeninseln",
|
||||
"North Korea": "Nordkorea",
|
||||
"Norfolk Island": "Norfolkinsel",
|
||||
"Niue": "Niue",
|
||||
"Nigeria": "Nigeria",
|
||||
"Niger": "Niger",
|
||||
"Nicaragua": "Nicaragua",
|
||||
"New Zealand": "Neuseeland",
|
||||
"New Caledonia": "Neukaledonien",
|
||||
"Netherlands": "Niederlande",
|
||||
"Nepal": "Nepal",
|
||||
"Nauru": "Nauru",
|
||||
"Namibia": "Namibia",
|
||||
"Myanmar": "Myanmar",
|
||||
"Mozambique": "Mosambik",
|
||||
"Morocco": "Marokko",
|
||||
"Montserrat": "Montserrat",
|
||||
"Montenegro": "Montenegro",
|
||||
"Mongolia": "Mongolei",
|
||||
"Czech Republic": "Tschechische Republik",
|
||||
"Monaco": "Monaco",
|
||||
"Moldova": "Moldawien",
|
||||
"Micronesia": "Mikronesien",
|
||||
"Mexico": "Mexiko",
|
||||
"Mayotte": "Mayotte",
|
||||
"Mauritius": "Mauritius",
|
||||
"Mauritania": "Mauretanien",
|
||||
"Martinique": "Martinique",
|
||||
"Marshall Islands": "Marshallinseln",
|
||||
"Malta": "Malta",
|
||||
"Mali": "Mali",
|
||||
"Maldives": "Malediven",
|
||||
"Malaysia": "Malaysia",
|
||||
"Malawi": "Malawi",
|
||||
"Madagascar": "Madagaskar",
|
||||
"Macedonia": "Mazedonien",
|
||||
"Macau": "Macau",
|
||||
"Luxembourg": "Luxemburg",
|
||||
"Lithuania": "Litauen",
|
||||
"Liechtenstein": "Liechtenstein",
|
||||
"Libya": "Libyen",
|
||||
"Liberia": "Liberia",
|
||||
"Lesotho": "Lesotho",
|
||||
"Lebanon": "Libanon",
|
||||
"Latvia": "Lettland",
|
||||
"Laos": "Laos",
|
||||
"Kyrgyzstan": "Kirgisistan",
|
||||
"Kuwait": "Kuwait",
|
||||
"Kosovo": "Kosovo",
|
||||
"Kiribati": "Kiribati",
|
||||
"Kenya": "Kenia",
|
||||
"Kazakhstan": "Kasachstan",
|
||||
"Jordan": "Jordanien",
|
||||
"Jersey": "Jersey",
|
||||
"Japan": "Japan",
|
||||
"Jamaica": "Jamaika",
|
||||
"Italy": "Italien",
|
||||
"Israel": "Israel",
|
||||
"Isle of Man": "Insel Man",
|
||||
"Ireland": "Irland",
|
||||
"Iraq": "Irak",
|
||||
"Iran": "Iran",
|
||||
"Indonesia": "Indonesien",
|
||||
"India": "Indien",
|
||||
"Iceland": "Island",
|
||||
"Hungary": "Ungarn",
|
||||
"Hong Kong": "Hongkong",
|
||||
"Honduras": "Honduras",
|
||||
"Heard & McDonald Islands": "Heard & McDonald-Inseln",
|
||||
"Haiti": "Haiti",
|
||||
"Guyana": "Guyana",
|
||||
"Guinea-Bissau": "Guinea-Bissau",
|
||||
"Guinea": "Guinea",
|
||||
"Guernsey": "Guernsey",
|
||||
"Guatemala": "Guatemala",
|
||||
"Guam": "Guam",
|
||||
"Guadeloupe": "Guadeloupe",
|
||||
"Grenada": "Grenada",
|
||||
"Greenland": "Grönland",
|
||||
"Greece": "Griechenland",
|
||||
"Gibraltar": "Gibraltar",
|
||||
"Ghana": "Ghana",
|
||||
"Germany": "Deutschland",
|
||||
"Georgia": "Georgien",
|
||||
"Gambia": "Gambia",
|
||||
"Gabon": "Gabun",
|
||||
"French Southern Territories": "Französische Süd-Territorien",
|
||||
"French Polynesia": "Französisch-Polynesien",
|
||||
"French Guiana": "Französisch-Guayana",
|
||||
"France": "Frankreich",
|
||||
"Finland": "Finnland",
|
||||
"Fiji": "Fidschi",
|
||||
"Faroe Islands": "Färöer-Inseln",
|
||||
"Falkland Islands": "Falklandinseln",
|
||||
"Ethiopia": "Äthiopien",
|
||||
"Estonia": "Estland",
|
||||
"Eritrea": "Eritrea",
|
||||
"Equatorial Guinea": "Äquatorialguinea",
|
||||
"El Salvador": "El Salvador",
|
||||
"Egypt": "Ägypten",
|
||||
"Ecuador": "Ecuador",
|
||||
"Dominican Republic": "Dominikanische Republik",
|
||||
"Dominica": "Dominica",
|
||||
"Djibouti": "Dschibuti",
|
||||
"Denmark": "Dänemark",
|
||||
"Côte d’Ivoire": "Tschechische Republik",
|
||||
"Cyprus": "Zypern",
|
||||
"Curaçao": "Curaçao",
|
||||
"Cuba": "Kuba",
|
||||
"Croatia": "Kroatien",
|
||||
"Cook Islands": "Cookinseln",
|
||||
"Costa Rica": "Costa Rica",
|
||||
"Congo - Kinshasa": "Republik Kongo - Kinshasa",
|
||||
"Congo - Brazzaville": "Republik Kongo - Brazzaville",
|
||||
"Comoros": "Komoren",
|
||||
"Colombia": "Kolumbien",
|
||||
"Cocos (Keeling) Islands": "Kokosinseln (Keeling)",
|
||||
"Christmas Island": "Weihnachtsinsel",
|
||||
"China": "China",
|
||||
"Chile": "Chile",
|
||||
"Chad": "Tschad",
|
||||
"Central African Republic": "Zentralafrikanische Republik",
|
||||
"Cayman Islands": "Kaimaninseln",
|
||||
"Caribbean Netherlands": "Karibische Niederlande",
|
||||
"Cape Verde": "Kap Verde",
|
||||
"Canada": "Kanada",
|
||||
"Cameroon": "Kamerun",
|
||||
"Cambodia": "Kambodscha",
|
||||
"Burundi": "Burundi",
|
||||
"Burkina Faso": "Burkina Faso",
|
||||
"Bulgaria": "Bulgarien",
|
||||
"Brunei": "Brunei",
|
||||
"British Virgin Islands": "Britische Jungferninseln",
|
||||
"British Indian Ocean Territory": "Britisches Territorium im Indischen Ozean",
|
||||
"Brazil": "Brasilien",
|
||||
"Bouvet Island": "Bouvetinsel",
|
||||
"Botswana": "Botswana",
|
||||
"Bosnia": "Bosnien",
|
||||
"Bolivia": "Bolivien",
|
||||
"Bhutan": "Bhutan",
|
||||
"Bermuda": "Bermuda",
|
||||
"Benin": "Benin",
|
||||
"Belize": "Belize",
|
||||
"Belgium": "Belgien",
|
||||
"Belarus": "Weißrussland",
|
||||
"Barbados": "Barbados",
|
||||
"Bangladesh": "Bangladesch",
|
||||
"Bahrain": "Bahrain",
|
||||
"Bahamas": "Bahamas",
|
||||
"Azerbaijan": "Aserbaidschan",
|
||||
"Austria": "Österreich",
|
||||
"Australia": "Australien",
|
||||
"Aruba": "Aruba",
|
||||
"Armenia": "Armenien",
|
||||
"Argentina": "Argentinien",
|
||||
"Antigua & Barbuda": "Antigua und Barbuda",
|
||||
"Antarctica": "Antarktis",
|
||||
"Anguilla": "Anguilla",
|
||||
"Angola": "Angola",
|
||||
"Andorra": "Andorra",
|
||||
"American Samoa": "Amerikanisch-Samoa",
|
||||
"Algeria": "Algerien",
|
||||
"Albania": "Albanien",
|
||||
"Åland Islands": "Äland-Inseln",
|
||||
"Afghanistan": "Afghanistan",
|
||||
"United States": "Vereinigte Staaten",
|
||||
"United Kingdom": "Großbritannien",
|
||||
"We call the places you where you can host your account ‘homeservers’.": "Orte, an denen du dein Benutzerkonto hosten kannst, nennen wir \"Homeserver\".",
|
||||
"Specify a homeserver": "Gib einen Homeserver an",
|
||||
"Render LaTeX maths in messages": "Zeige LaTeX-Matheformeln in Nachrichten an",
|
||||
"Decide where your account is hosted": "Gib an wo dein Benutzerkonto gehostet werden soll",
|
||||
"Already have an account? <a>Sign in here</a>": "Hast du schon ein Benutzerkonto? <a>Melde dich hier an</a>",
|
||||
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s oder %(usernamePassword)s",
|
||||
"Continue with %(ssoButtons)s": "Mit %(ssoButtons)s fortfahren",
|
||||
"That username already exists, please try another.": "Dieser Benutzername existiert schon. Bitte versuche es mit einem anderen.",
|
||||
"New? <a>Create account</a>": "Neu? <a>Erstelle ein Benutzerkonto</a>",
|
||||
"There was a problem communicating with the homeserver, please try again later.": "Es gab ein Problem bei der Kommunikation mit dem Homseserver. Bitte versuche es später erneut.",
|
||||
"New here? <a>Create an account</a>": "Neu hier? <a>Erstelle ein Benutzerkonto</a>",
|
||||
"Got an account? <a>Sign in</a>": "Hast du schon ein Benutzerkonto? <a>Melde dich an</a>",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Nutze optional eine E-Mail-Adresse, um von Nutzern gefunden werden zu können.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Nutze optional eine E-Mail-Adresse oder Telefonnummer, um von Nutzern gefunden werden zu können.",
|
||||
"Add an email to be able to reset your password.": "Füge eine E-Mail-Adresse hinzu, um dein Passwort zurücksetzen zu können.",
|
||||
"Forgot password?": "Passwort vergessen?",
|
||||
"That phone number doesn't look quite right, please check and try again": "Diese Telefonummer sieht nicht ganz richtig aus. Bitte überprüfe deine Eingabe und versuche es erneut",
|
||||
"About homeservers": "Über Homeserver",
|
||||
"Learn more": "Mehr dazu",
|
||||
"Use your preferred Matrix homeserver if you have one, or host your own.": "Verwende einen Matrix-Homeserver deiner Wahl oder hoste deinen eigenen.",
|
||||
"Other homeserver": "Anderer Homeserver",
|
||||
"Sign into your homeserver": "Melde dich bei deinem Homeserver an",
|
||||
"Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org ist der größte öffentliche Homeserver der Welt und daher ein guter Ort für viele.",
|
||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Aufgepasst: Wenn du keine E-Mail-Adresse angibst und dein Passwort vergisst, kannst du den <b>Zugriff auf deinen Account dauerhaft verlieren</b>.",
|
||||
"Continuing without email": "Ohne E-Mail fortfahren",
|
||||
"Reason (optional)": "Grund (optional)",
|
||||
"Continue with %(provider)s": "Mit %(provider)s fortfahren",
|
||||
"Homeserver": "Heimserver",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "Du kannst in den benutzerdefinierten Serveroptionen eine andere Heimserver-URL angeben, um dich bei anderen Matrixservern anzumelden.",
|
||||
"Server Options": "Servereinstellungen",
|
||||
"No other application is using the webcam": "Keine andere Anwendung auf die Webcam zugreift",
|
||||
"Permission is granted to use the webcam": "Auf die Webcam zugegriffen werden darf",
|
||||
"A microphone and webcam are plugged in and set up correctly": "Mikrofon und Webcam eingesteckt und richtig eingerichtet sind",
|
||||
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon zugegriffen werden konnte. Stelle sicher, dass das Mikrofon richtig eingesteckt und eingerichtet ist.",
|
||||
"Call failed because no webcam or microphone could not be accessed. Check that:": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon oder die Webcam zugegriffen werden konnte. Stelle sicher, dass:",
|
||||
"Unable to access webcam / microphone": "Auf Webcam / Mikrofon konnte nicht zugegriffen werden",
|
||||
"Unable to access microphone": "Es konnte nicht auf das Mikrofon zugegriffen werden"
|
||||
}
|
||||
|
|
|
@ -46,11 +46,18 @@
|
|||
"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.": "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.",
|
||||
"Try using turn.matrix.org": "Try using turn.matrix.org",
|
||||
"OK": "OK",
|
||||
"Unable to access microphone": "Unable to access microphone",
|
||||
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.",
|
||||
"Unable to access webcam / microphone": "Unable to access webcam / microphone",
|
||||
"Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:",
|
||||
"A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
|
||||
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
|
||||
"No other application is using the webcam": "No other application is using the webcam",
|
||||
"Unable to capture screen": "Unable to capture screen",
|
||||
"Existing Call": "Existing Call",
|
||||
"You are already in a call.": "You are already in a call.",
|
||||
"VoIP is unsupported": "VoIP is unsupported",
|
||||
"You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.",
|
||||
"Too Many Calls": "Too Many Calls",
|
||||
"You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.",
|
||||
"You cannot place a call with yourself.": "You cannot place a call with yourself.",
|
||||
"Call in Progress": "Call in Progress",
|
||||
"A call is currently being placed!": "A call is currently being placed!",
|
||||
|
@ -399,6 +406,7 @@
|
|||
"Messages": "Messages",
|
||||
"Actions": "Actions",
|
||||
"Advanced": "Advanced",
|
||||
"Effects": "Effects",
|
||||
"Other": "Other",
|
||||
"Command error": "Command error",
|
||||
"Usage": "Usage",
|
||||
|
@ -812,13 +820,13 @@
|
|||
"Show hidden events in timeline": "Show hidden events in timeline",
|
||||
"Low bandwidth mode": "Low bandwidth mode",
|
||||
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
|
||||
"Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
|
||||
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
|
||||
"Enable message search in encrypted rooms": "Enable message search in encrypted rooms",
|
||||
"How fast should messages be downloaded.": "How fast should messages be downloaded.",
|
||||
"Manually verify all remote sessions": "Manually verify all remote sessions",
|
||||
"IRC display name width": "IRC display name width",
|
||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||
"Show chat effects": "Show chat effects",
|
||||
"Collecting app version information": "Collecting app version information",
|
||||
"Collecting logs": "Collecting logs",
|
||||
"Uploading logs": "Uploading logs",
|
||||
|
@ -837,10 +845,15 @@
|
|||
"When rooms are upgraded": "When rooms are upgraded",
|
||||
"My Ban List": "My Ban List",
|
||||
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
||||
"Sends the given message with confetti": "Sends the given message with confetti",
|
||||
"sends confetti": "sends confetti",
|
||||
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||
"Video Call": "Video Call",
|
||||
"Voice Call": "Voice Call",
|
||||
"Fill Screen": "Fill Screen",
|
||||
"Return to call": "Return to call",
|
||||
"%(name)s paused": "%(name)s paused",
|
||||
"Unknown caller": "Unknown caller",
|
||||
"Incoming voice call": "Incoming voice call",
|
||||
"Incoming video call": "Incoming video call",
|
||||
|
@ -1129,6 +1142,8 @@
|
|||
"Message layout": "Message layout",
|
||||
"Compact": "Compact",
|
||||
"Modern": "Modern",
|
||||
"Hide advanced": "Hide advanced",
|
||||
"Show advanced": "Show advanced",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
||||
"Customise your appearance": "Customise your appearance",
|
||||
"Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.",
|
||||
|
@ -1888,6 +1903,11 @@
|
|||
"This address is available to use": "This address is available to use",
|
||||
"This address is already in use": "This address is already in use",
|
||||
"Room directory": "Room directory",
|
||||
"Server Options": "Server Options",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.",
|
||||
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
||||
"Homeserver": "Homeserver",
|
||||
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||
"And %(count)s more...|other": "And %(count)s more...",
|
||||
"Home": "Home",
|
||||
|
@ -1948,6 +1968,7 @@
|
|||
"Removing…": "Removing…",
|
||||
"Confirm Removal": "Confirm Removal",
|
||||
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.",
|
||||
"Reason (optional)": "Reason (optional)",
|
||||
"Clear all data in this session?": "Clear all data in this session?",
|
||||
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.",
|
||||
"Clear all data": "Clear all data",
|
||||
|
@ -1982,8 +2003,6 @@
|
|||
"Name": "Name",
|
||||
"Topic (optional)": "Topic (optional)",
|
||||
"Make this room public": "Make this room public",
|
||||
"Hide advanced": "Hide advanced",
|
||||
"Show advanced": "Show advanced",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
|
||||
"Create Room": "Create Room",
|
||||
"Sign out": "Sign out",
|
||||
|
@ -2106,6 +2125,10 @@
|
|||
"Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:",
|
||||
"If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.",
|
||||
"This wasn't me": "This wasn't me",
|
||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||
"Continuing without email": "Continuing without email",
|
||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
|
||||
"Email (optional)": "Email (optional)",
|
||||
"Please fill why you're reporting.": "Please fill why you're reporting.",
|
||||
"Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
|
||||
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
|
||||
|
@ -2139,6 +2162,16 @@
|
|||
"A connection error occurred while trying to contact the server.": "A connection error occurred while trying to contact the server.",
|
||||
"The server is not configured to indicate what the problem is (CORS).": "The server is not configured to indicate what the problem is (CORS).",
|
||||
"Recent changes that have not yet been received": "Recent changes that have not yet been received",
|
||||
"Unable to validate homeserver": "Unable to validate homeserver",
|
||||
"Invalid URL": "Invalid URL",
|
||||
"Specify a homeserver": "Specify a homeserver",
|
||||
"Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.",
|
||||
"Sign into your homeserver": "Sign into your homeserver",
|
||||
"We call the places where you can host your account ‘homeservers’.": "We call the places where you can host your account ‘homeservers’.",
|
||||
"Other homeserver": "Other homeserver",
|
||||
"Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
|
||||
"Learn more": "Learn more",
|
||||
"About homeservers": "About homeservers",
|
||||
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
|
||||
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
||||
"Send Logs": "Send Logs",
|
||||
|
@ -2232,6 +2265,8 @@
|
|||
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
|
||||
"Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
|
||||
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
|
||||
"Resume": "Resume",
|
||||
"Hold": "Hold",
|
||||
"Reject invitation": "Reject invitation",
|
||||
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
|
||||
"Unable to reject invite": "Unable to reject invite",
|
||||
|
@ -2267,8 +2302,6 @@
|
|||
"powered by Matrix": "powered by Matrix",
|
||||
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
|
||||
"Country Dropdown": "Country Dropdown",
|
||||
"Custom Server Options": "Custom Server Options",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.",
|
||||
"Confirm your identity by entering your account password below.": "Confirm your identity by entering your account password below.",
|
||||
"Password": "Password",
|
||||
"Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.",
|
||||
|
@ -2282,48 +2315,30 @@
|
|||
"Code": "Code",
|
||||
"Submit": "Submit",
|
||||
"Start authentication": "Start authentication",
|
||||
"Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
|
||||
"Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of <a>element.io</a>.": "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of <a>element.io</a>.",
|
||||
"Server Name": "Server Name",
|
||||
"Enter password": "Enter password",
|
||||
"Nice, strong password!": "Nice, strong password!",
|
||||
"Password is allowed, but unsafe": "Password is allowed, but unsafe",
|
||||
"Keep going...": "Keep going...",
|
||||
"Enter username": "Enter username",
|
||||
"Enter email address": "Enter email address",
|
||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||
"Enter phone number": "Enter phone number",
|
||||
"Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
|
||||
"That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again",
|
||||
"Email": "Email",
|
||||
"Username": "Username",
|
||||
"Phone": "Phone",
|
||||
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
|
||||
"Forgot password?": "Forgot password?",
|
||||
"Sign in with": "Sign in with",
|
||||
"Sign in": "Sign in",
|
||||
"No identity server is configured so you cannot add an email address in order to reset your password in the future.": "No identity server is configured so you cannot add an email address in order to reset your password in the future.",
|
||||
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
|
||||
"Use an email address to recover your account": "Use an email address to recover your account",
|
||||
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
|
||||
"Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
|
||||
"Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
|
||||
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
|
||||
"Email (optional)": "Email (optional)",
|
||||
"Phone (optional)": "Phone (optional)",
|
||||
"Register": "Register",
|
||||
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.",
|
||||
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.",
|
||||
"Enter your custom homeserver URL <a>What does this mean?</a>": "Enter your custom homeserver URL <a>What does this mean?</a>",
|
||||
"Homeserver URL": "Homeserver URL",
|
||||
"Enter your custom identity server URL <a>What does this mean?</a>": "Enter your custom identity server URL <a>What does this mean?</a>",
|
||||
"Identity Server URL": "Identity Server URL",
|
||||
"Other servers": "Other servers",
|
||||
"Free": "Free",
|
||||
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
||||
"Premium": "Premium",
|
||||
"Premium hosting for organisations <a>Learn more</a>": "Premium hosting for organisations <a>Learn more</a>",
|
||||
"Find other public servers or use a custom server": "Find other public servers or use a custom server",
|
||||
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
|
||||
"Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
|
||||
"Add an email to be able to reset your password.": "Add an email to be able to reset your password.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Use email or phone to optionally be discoverable by existing contacts.",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.",
|
||||
"Sign in with SSO": "Sign in with SSO",
|
||||
"Couldn't load page": "Couldn't load page",
|
||||
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
|
||||
|
@ -2485,13 +2500,10 @@
|
|||
"A new password must be entered.": "A new password must be entered.",
|
||||
"New passwords must match each other.": "New passwords must match each other.",
|
||||
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.",
|
||||
"Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s",
|
||||
"Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
|
||||
"No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.",
|
||||
"Sign in instead": "Sign in instead",
|
||||
"New Password": "New Password",
|
||||
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
|
||||
"Send Reset Email": "Send Reset Email",
|
||||
"Sign in instead": "Sign in instead",
|
||||
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
|
||||
"I have verified my email address": "I have verified my email address",
|
||||
"Your password has been reset.": "Your password has been reset.",
|
||||
|
@ -2513,24 +2525,28 @@
|
|||
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
|
||||
"Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
|
||||
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
|
||||
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
|
||||
"There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.",
|
||||
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
"Syncing...": "Syncing...",
|
||||
"Signing In...": "Signing In...",
|
||||
"If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while",
|
||||
"Create account": "Create account",
|
||||
"New? <a>Create account</a>": "New? <a>Create account</a>",
|
||||
"Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
|
||||
"Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
|
||||
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
|
||||
"That username already exists, please try another.": "That username already exists, please try another.",
|
||||
"Continue with %(ssoButtons)s": "Continue with %(ssoButtons)s",
|
||||
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Or %(usernamePassword)s",
|
||||
"Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
|
||||
"Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
|
||||
"Continue with previous account": "Continue with previous account",
|
||||
"<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
|
||||
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
|
||||
"Registration Successful": "Registration Successful",
|
||||
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
|
||||
"Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
|
||||
"Create your account": "Create your account",
|
||||
"Create account": "Create account",
|
||||
"Host account on": "Host account on",
|
||||
"Decide where your account is hosted": "Decide where your account is hosted",
|
||||
"Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
|
||||
"Use Recovery Key": "Use Recovery Key",
|
||||
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",
|
||||
|
|
|
@ -526,7 +526,7 @@
|
|||
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s eemaldas täiendava aadressi %(addresses)s sellelt jututoalt.",
|
||||
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s muutis selle jututoa täiendavat aadressi.",
|
||||
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s muutis selle jututoa põhiaadressi ja täiendavat aadressi.",
|
||||
"%(senderName)s changed the addresses for this room.": "%(senderName)s muutis selle jututoa aadressid.",
|
||||
"%(senderName)s changed the addresses for this room.": "%(senderName)s muutis selle jututoa aadresse.",
|
||||
"Someone": "Keegi",
|
||||
"(not supported by this browser)": "(ei ole toetatud selles brauseris)",
|
||||
"(could not connect media)": "(ühendus teise osapoolega ei õnnestunud)",
|
||||
|
@ -1016,7 +1016,7 @@
|
|||
"Community %(groupId)s not found": "%(groupId)s kogukonda ei leidunud",
|
||||
"This homeserver does not support communities": "See koduserver ei toeta kogukondade funktsionaalsust",
|
||||
"Failed to load %(groupId)s": "%(groupId)s kogukonna laadimine ei õnnestunud",
|
||||
"Welcome to %(appName)s": "Tere tulemast %(appName)s kasutajaks",
|
||||
"Welcome to %(appName)s": "Tere tulemast suhtlusrakenduse %(appName)s kasutajaks",
|
||||
"Liberate your communication": "Vabasta oma suhtlus",
|
||||
"Send a Direct Message": "Saada otsesõnum",
|
||||
"Are you sure you want to leave the room '%(roomName)s'?": "Kas oled kindel, et soovid lahkuda jututoast '%(roomName)s'?",
|
||||
|
@ -1594,11 +1594,11 @@
|
|||
"All settings": "Kõik seadistused",
|
||||
"Feedback": "Tagasiside",
|
||||
"Use Single Sign On to continue": "Jätkamiseks kasuta ühekordset sisselogimist",
|
||||
"Confirm adding this email address by using Single Sign On to prove your identity.": "Kinnita selle e-posti aadressi lisamine kasutades ühekordset sisselogimist oma isiku tuvastamiseks.",
|
||||
"Confirm adding this email address by using Single Sign On to prove your identity.": "Kinnita selle e-posti aadress kasutades oma isiku tuvastamiseks ühekordset sisselogimist (Single Sign On).",
|
||||
"Single Sign On": "SSO Ühekordne sisselogimine",
|
||||
"Confirm adding email": "Kinnita e-posti aadressi lisamine",
|
||||
"Click the button below to confirm adding this email address.": "Klõpsi järgnevat nuppu e-posti aadressi lisamise kinnitamiseks.",
|
||||
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Kinnita selle telefoninumbri lisamine kasutades ühekordset sisselogimist oma isiku tuvastamiseks.",
|
||||
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Kinnita selle telefoninumbri lisamine kasutades oma isiku tuvastamiseks ühekordset sisselogimist (Single Sign On).",
|
||||
"Confirm adding phone number": "Kinnita telefoninumbri lisamine",
|
||||
"Click the button below to confirm adding this phone number.": "Klõpsi järgnevat nuppu telefoninumbri lisamise kinnitamiseks.",
|
||||
"Add Phone Number": "Lisa telefoninumber",
|
||||
|
@ -2399,7 +2399,7 @@
|
|||
"A connection error occurred while trying to contact the server.": "Serveriga ühenduse algatamisel tekkis viga.",
|
||||
"The server is not configured to indicate what the problem is (CORS).": "Server on seadistatud varjama tegelikke veapõhjuseid (CORS).",
|
||||
"No files visible in this room": "Selles jututoas pole nähtavaid faile",
|
||||
"Attach files from chat or just drag and drop them anywhere in a room.": "Faile saad manueks lisada kas vastava nupu alt vestlusest või sikutades neid jututoa aknasse.",
|
||||
"Attach files from chat or just drag and drop them anywhere in a room.": "Faile saad manuseks lisada kas vastava nupu alt vestlusest või sikutades neid jututoa aknasse.",
|
||||
"You have no visible notifications in this room.": "Jututoas pole nähtavaid teavitusi.",
|
||||
"You're all caught up.": "Ei tea... kõik vist on nüüd tehtud.",
|
||||
"You’re all caught up": "Ei tea... kõik vist on nüüd tehtud",
|
||||
|
@ -2846,5 +2846,131 @@
|
|||
"Filter rooms and people": "Otsi jututubasid ja inimesi",
|
||||
"Open the link in the email to continue registration.": "Registreerimisega jätkamiseks vajuta e-kirjas olevat linki.",
|
||||
"A confirmation email has been sent to %(emailAddress)s": "Saatsime kinnituskirja %(emailAddress)s aadressile",
|
||||
"Start a new chat": "Alusta uut vestlust"
|
||||
"Start a new chat": "Alusta uut vestlust",
|
||||
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>Sinu kogukonna lehe HTML'i näidis - see on pealkiri</h1>\n<p>\n Tutvustamaks uutele liikmetele kogukonda, kasuta seda pikka kirjeldust\n või jaga olulist teavet <a href=\"foo\">viidetena</a>\n</p>\n<p>\n Pildite lisaminseks võid sa isegi kasutada img-märgendit Matrix'i url'idega <img src=\"mxc://url\" />\n</p>\n",
|
||||
"Go to Home View": "Avalehele",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(rooms)s jututoa andmete salvestamiseks kulub hetkel %(size)s.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(rooms)s jututoa andmete salvestamiseks kulub hetkel %(size)s.",
|
||||
"This widget would like to:": "See vidin sooviks:",
|
||||
"Approve widget permissions": "Anna vidinale õigused",
|
||||
"Use Ctrl + Enter to send a message": "Sõnumi saatmiseks vajuta Ctrl + Enter",
|
||||
"Decline All": "Keeldu kõigist",
|
||||
"Approve": "Nõustu",
|
||||
"Remain on your screen when viewing another room, when running": "Kui vaatad mõnda teist jututuba, siis jää oma ekraanivaate juurde",
|
||||
"Remain on your screen while running": "Jää oma ekraanivaate juurde",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "Saada enda nimel <b>%(eventType)s</b> sündmusi siia jututuppa",
|
||||
"with state key %(stateKey)s": "olekuvõtmega %(stateKey)s",
|
||||
"with an empty state key": "tühja olekuvõtmega",
|
||||
"See when anyone posts a sticker to your active room": "Vaata kui keegi on saatnud kleepse aktiivsesse jututuppa",
|
||||
"Send stickers to your active room as you": "Saada enda nimel kleepse hetkel aktiivsesse jututuppa",
|
||||
"See when a sticker is posted in this room": "Vaata kui uus kleeps on siia jututuppa lisatud",
|
||||
"Send stickers to this room as you": "Saada sellesse jututuppa kleepse iseendana",
|
||||
"See when the avatar changes in your active room": "Vaata kui hetkel aktiivse jututoa tunnuspilt muutub",
|
||||
"Change the avatar of your active room": "Muuda oma aktiivse jututoa tunnuspilti",
|
||||
"See when the avatar changes in this room": "Vaata kui selle jututoa tunnuspilt muutub",
|
||||
"Change the avatar of this room": "Muuda selle jututoa tunnuspilti",
|
||||
"See when the name changes in your active room": "Vaata kui hetkel aktiivse jututoa nimi muutub",
|
||||
"Change the name of your active room": "Muuda oma aktiivse jututoa nime",
|
||||
"See when the name changes in this room": "Vaata kui selle jututoa nimi muutub",
|
||||
"Change the name of this room": "Muuda selle jututoa nime",
|
||||
"See when the topic changes in your active room": "Vaata kui hetkel aktiivse jututoa teema muutub",
|
||||
"See when the topic changes in this room": "Vaata kui selle jututoa teema muutub",
|
||||
"Change the topic of your active room": "Muuda oma aktiivse jututoa teemat",
|
||||
"Change the topic of this room": "Muuda selle jututoa teemat",
|
||||
"Change which room you're viewing": "Vaheta vaadatavat jututuba",
|
||||
"Send stickers into your active room": "Saada kleepse hetkel aktiivsesse jututuppa",
|
||||
"Send stickers into this room": "Saada kleepse siia jututuppa",
|
||||
"See text messages posted to this room": "Vaata selle jututoa tekstisõnumeid",
|
||||
"Send text messages as you in your active room": "Saada oma aktiivses jututoas enda nimel tekstisõnumeid",
|
||||
"Send text messages as you in this room": "Saada selles jututoas oma nimel tekstisõnumeid",
|
||||
"See messages posted to your active room": "Vaata sõnumeid oma aktiivses jututoas",
|
||||
"See messages posted to this room": "Vaata selle jututoa sõnumeid",
|
||||
"Send messages as you in your active room": "Saada oma aktiivses jututoas enda nimel sõnumeid",
|
||||
"Send messages as you in this room": "Saada selles jututoas oma nimel sõnumeid",
|
||||
"The <b>%(capability)s</b> capability": "<b>%(capability)s</b> võimekus",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "Vaata oma aktiivsesse jututuppa saadetud <b>%(eventType)s</b> sündmusi",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "Saada oma nimel oma aktiivses jututoas <b>%(eventType)s</b> sündmusi",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "Vaata siia jututuppa saadetud <b>%(eventType)s</b> sündmusi",
|
||||
"Enter phone number": "Sisesta telefoninumber",
|
||||
"Enter email address": "Sisesta e-posti aadress",
|
||||
"Return to call": "Pöördu tagasi kõne juurde",
|
||||
"Fill Screen": "Täida ekraan",
|
||||
"Voice Call": "Häälkõne",
|
||||
"Video Call": "Videokõne",
|
||||
"Use Command + Enter to send a message": "Sõnumi saatmiseks vajuta Command + Enter klahve",
|
||||
"See <b>%(msgtype)s</b> messages posted to your active room": "Näha sinu aktiivsesse jututuppa saadetud <b>%(msgtype)s</b> sõnumeid",
|
||||
"See <b>%(msgtype)s</b> messages posted to this room": "Näha sellesse jututuppa saadetud <b>%(msgtype)s</b> sõnumeid",
|
||||
"Send <b>%(msgtype)s</b> messages as you in your active room": "Saata sinu nimel <b>%(msgtype)s</b> sõnumeid sinu aktiivsesse jututuppa",
|
||||
"Send <b>%(msgtype)s</b> messages as you in this room": "Saata sinu nimel <b>%(msgtype)s</b> sõnumeid siia jututuppa",
|
||||
"See general files posted to your active room": "Näha sinu aktiivsesse jututuppa lisatud muid faile",
|
||||
"See general files posted to this room": "Näha sellesse jututuppa lisatud muid faile",
|
||||
"Send general files as you in your active room": "Saata sinu nimel muid faile sinu aktiivsesse jututuppa",
|
||||
"Send general files as you in this room": "Saata sinu nimel muid faile siia jututuppa",
|
||||
"See videos posted to your active room": "Näha videosid sinu aktiivses jututoas",
|
||||
"See videos posted to this room": "Näha siia jututuppa lisatud videosid",
|
||||
"Send videos as you in your active room": "Saata sinu nimel videosid sinu aktiivsesse jututuppa",
|
||||
"Send videos as you in this room": "Saata sinu nimel videosid siia jututuppa",
|
||||
"See images posted to your active room": "Näha sinu aktiivsesse jututuppa lisatud pilte",
|
||||
"See images posted to this room": "Näha siia jututuppa lisatud pilte",
|
||||
"Send images as you in your active room": "Saata sinu nimel pilte sinu aktiivsesse jututuppa",
|
||||
"Send images as you in this room": "Saata sinu nimel pilte siia jututuppa",
|
||||
"See emotes posted to your active room": "Näha emotesid sinu aktiivses jututoas",
|
||||
"See emotes posted to this room": "Vaata selle jututoa emotesid",
|
||||
"Send emotes as you in your active room": "Saada oma aktiivses jututoas enda nimel emotesid",
|
||||
"Send emotes as you in this room": "Saada selles jututoas oma nimel emotesid",
|
||||
"See text messages posted to your active room": "Vaata tekstisõnumeid oma aktiivses jututoas",
|
||||
"New here? <a>Create an account</a>": "Täitsa uus asi sinu jaoks? <a>Loo omale kasutajakonto</a>",
|
||||
"Got an account? <a>Sign in</a>": "Sul on kasutajakonto olemas? <a>Siis logi sisse</a>",
|
||||
"Render LaTeX maths in messages": "Sõnumites visualiseeri LaTeX-vormingus matemaatikat",
|
||||
"No other application is using the webcam": "Ainsamgi muu rakendus ei kasuta veebikaamerat",
|
||||
"Permission is granted to use the webcam": "Rakendusel õigus veebikaamerat kasutada",
|
||||
"A microphone and webcam are plugged in and set up correctly": "Veebikaamera ja mikrofon oleks ühendatud ja seadistatud",
|
||||
"Call failed because no webcam or microphone could not be accessed. Check that:": "Kuna veebikaamerat või mikrofoni kasutada ei saanud, siis kõne ei õnnestunud. Palun kontrolli, et:",
|
||||
"Unable to access webcam / microphone": "Puudub ligipääs veebikaamerale ja mikrofonile",
|
||||
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Kuna mikrofoni kasutada ei saanud, siis kõne ei õnnestunud. Palun kontrolli, et mikrofon oleks ühendatud ja seadistatud.",
|
||||
"Unable to access microphone": "Puudub ligipääs mikrofonile",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "Serveri seadistusi muutes võid teise koduserveri aadressi sisestamisel logida sisse muudesse Matrix'i serveritesse. See võimaldab sul vestlusrakenduses Element kasutada olemasolevat kasutajakontot teises koduserveris.",
|
||||
"Continuing without email": "Jätka ilma e-posti aadressi seadistamiseta",
|
||||
"Continue with %(provider)s": "Jätka %(provider)s kasutamist",
|
||||
"Homeserver": "Koduserver",
|
||||
"Server Options": "Serveri seadistused",
|
||||
"Host account on": "Sinu kasutajakontot teenindab",
|
||||
"Decide where your account is hosted": "Vali kes võiks sinu kasutajakontot teenindada",
|
||||
"Already have an account? <a>Sign in here</a>": "Sul juba on kasutajakonto olemas? <a>Logi siin sisse</a>",
|
||||
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s või %(usernamePassword)s",
|
||||
"Continue with %(ssoButtons)s": "Jätkamiseks kasuta %(ssoButtons)s teenuseid",
|
||||
"That username already exists, please try another.": "Selline kasutajanimi on juba olemas, palun vali midagi muud.",
|
||||
"New? <a>Create account</a>": "Täitsa uus asi sinu jaoks? <a>Loo omale kasutajakonto</a>",
|
||||
"There was a problem communicating with the homeserver, please try again later.": "Serveriühenduses tekkis viga. Palun proovi mõne aja pärast uuesti.",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Kui soovid, et teised kasutajad saaksid sind leida, siis palun lisa oma e-posti aadress.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Kui soovid, et teised kasutajad saaksid sind leida, siis palun lisa oma e-posti aadress või telefoninumber.",
|
||||
"Add an email to be able to reset your password.": "Selleks et saaksid vajadusel oma salasõna muuta, palun lisa oma e-posti aadress.",
|
||||
"Forgot password?": "Kas unustasid oma salasõna?",
|
||||
"That phone number doesn't look quite right, please check and try again": "See telefoninumber ei tundu õige olema, palun kontrolli ta üle ja proovi uuesti",
|
||||
"About homeservers": "Teave koduserverite kohta",
|
||||
"Learn more": "Loe veel",
|
||||
"Use your preferred Matrix homeserver if you have one, or host your own.": "Kui sul on oma koduserveri eelistus olemas, siis kasuta seda. Samuti võid soovi korral oma enda koduserveri püsti panna.",
|
||||
"Other homeserver": "Muu koduserver",
|
||||
"We call the places you where you can host your account ‘homeservers’.": "Me nimetame „koduserveriks“ sellist serverit, mis haldab sinu kasutajakontot.",
|
||||
"Sign into your homeserver": "Logi sisse oma koduserverisse",
|
||||
"Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org on maailma suurim avalik koduserver ja see sobib paljude jaoks.",
|
||||
"Specify a homeserver": "Sisesta koduserver",
|
||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Lihtsalt hoiatame, et kui sa ei lisa e-posti aadressi ning unustad oma konto salasõna, siis sa võid <b>püsivalt kaotada ligipääsu oma kontole</b>.",
|
||||
"Reason (optional)": "Põhjus (kui soovid lisada)",
|
||||
"We call the places where you can host your account ‘homeservers’.": "Me nimetame „koduserveriks“ sellist serverit, mis haldab sinu kasutajakontot.",
|
||||
"Invalid URL": "Vigane aadress",
|
||||
"Unable to validate homeserver": "Koduserveri õigsust ei õnnestunud kontrollida",
|
||||
"Call failed because webcam or microphone could not be accessed. Check that:": "Kuna veebikaamerat või mikrofoni kasutada ei saanud, siis kõne ei õnnestunud. Palun kontrolli, et:",
|
||||
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Kuna mikrofoni kasutada ei saanud, siis kõne ei õnnestunud. Palun kontrolli, et mikrofon oleks ühendatud ja seadistatud.",
|
||||
"sends confetti": "saatis serpentiine",
|
||||
"Sends the given message with confetti": "Lisab sellele sõnumile serpentiine",
|
||||
"Show chat effects": "Näita vestlustes efekte",
|
||||
"Effects": "Vahvad täiendused",
|
||||
"Hold": "Pane ootele",
|
||||
"Resume": "Jätka",
|
||||
"%(peerName)s held the call": "%(peerName)s pani kõne ootele",
|
||||
"You held the call <a>Resume</a>": "Sa panid kõne ootele. <a>Jätka kõnet</a>",
|
||||
"You've reached the maximum number of simultaneous calls.": "Oled jõudnud suurima lubatud samaaegsete kõnede arvuni.",
|
||||
"%(name)s paused": "%(name)s peatas ajutiselt kõne",
|
||||
"Too Many Calls": "Liiga palju kõnesid"
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
"Collecting logs": "درحال جمعآوری گزارشها",
|
||||
"Search": "جستجو",
|
||||
"(HTTP status %(httpStatus)s)": "(HTTP وضعیت %(httpStatus)s)",
|
||||
"Failed to forget room %(errCode)s": "فراموش کردن گپگاه %(errCode)s موفقیتآمیز نبود",
|
||||
"Failed to forget room %(errCode)s": "فراموش کردن اتاق با خطا مواجه شد %(errCode)s",
|
||||
"Wednesday": "چهارشنبه",
|
||||
"Quote": "گفتآورد",
|
||||
"Send": "ارسال",
|
||||
|
@ -158,5 +158,69 @@
|
|||
"Whether or not you're logged in (we don't record your username)": "وارد حساب خود میشوید یا خیر (ما نام کاربری شما را ثبت نمیکنیم)",
|
||||
"Click the button below to confirm adding this phone number.": "برای تائید اضافهشدن این شماره تلفن، بر روی دکمهی زیر کلیک کنید.",
|
||||
"Confirm adding this phone number by using Single Sign On to prove your identity.": "برای اثبات هویت خود، اضافهشدن این شماره تلفن را با استفاده از Single Sign On تائید کنید.",
|
||||
"Failed to verify email address: make sure you clicked the link in the email": "خطا در تائید آدرس ایمیل: مطمئن شوید که بر روی لینک موجود در ایمیل کلیک کرده اید"
|
||||
"Failed to verify email address: make sure you clicked the link in the email": "خطا در تائید آدرس ایمیل: مطمئن شوید که بر روی لینک موجود در ایمیل کلیک کرده اید",
|
||||
"Forget room": "فراموش کردن اتاق",
|
||||
"Filter room members": "فیلتر کردن اعضای اتاق",
|
||||
"Failure to create room": "ایجاد اتاق با خطا مواجه شد",
|
||||
"Failed to upload profile picture!": "آپلود عکس پروفایل با خطا مواجه شد!",
|
||||
"Failed to unban": "رفع مسدودیت با خطا مواجه شد",
|
||||
"Failed to set display name": "تنظیم نام نمایشی با خطا مواجه شد",
|
||||
"Failed to send request.": "ارسال درخواست با خطا مواجه شد.",
|
||||
"Failed to send email": "ارسال ایمیل با خطا مواجه شد",
|
||||
"Failed to join room": "پیوستن به اتاق انجام نشد",
|
||||
"Failed to ban user": "کاربر مسدود نشد",
|
||||
"Error decrypting attachment": "خطا در رمزگشایی پیوست",
|
||||
"%(senderName)s ended the call.": "%(senderName)s به تماس پایان داد.",
|
||||
"Emoji": "شکلک",
|
||||
"Email address": "آدرس ایمیل",
|
||||
"Email": "ایمیل",
|
||||
"Drop File Here": "پرونده را اینجا رها کنید",
|
||||
"Download %(text)s": "دانلود 2%(text)s",
|
||||
"Disinvite": "پسگرفتن دعوت",
|
||||
"Default": "پیشفرض",
|
||||
"Deops user with given id": "کاربر را با شناسه داده شده را از بین می برد",
|
||||
"Decrypt %(text)s": "رمزگشایی %(text)s",
|
||||
"Deactivate Account": "غیرفعال کردن حساب",
|
||||
"/ddg is not a command": "/ddg یک فرمان نیست",
|
||||
"Current password": "گذرواژه فعلی",
|
||||
"Cryptography": "رمزنگاری",
|
||||
"Create Room": "ایجاد اتاق",
|
||||
"Confirm password": "تأیید گذرواژه",
|
||||
"Commands": "فرمانها",
|
||||
"Command error": "خطای فرمان",
|
||||
"Click here to fix": "برای رفع مشکل اینجا کلیک کنید",
|
||||
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s نام اتاق را حذف کرد.",
|
||||
"%(senderName)s changed their profile picture.": "%(senderName)s عکس پروفایل خود را تغییر داد.",
|
||||
"Change Password": "تغییر گذواژه",
|
||||
"Banned users": "کاربران مسدود شده",
|
||||
"Autoplay GIFs and videos": "پخش خودکار GIF و فیلم",
|
||||
"Attachment": "پیوست",
|
||||
"Are you sure you want to reject the invitation?": "آیا مطمئن هستید که می خواهید دعوت را رد کنید؟",
|
||||
"Are you sure you want to leave the room '%(roomName)s'?": "آیا مطمئن هستید که می خواهید از اتاق '2%(roomName)s' خارج شوید؟",
|
||||
"Are you sure?": "مطمئنی؟",
|
||||
"Anyone who knows the room's link, including guests": "هرکسی که لینک اتاق را می داند و کاربران مهمان",
|
||||
"Anyone who knows the room's link, apart from guests": "هرکسی که لینک اتاق را میداند، غیر از کاربران مهمان",
|
||||
"Anyone": "هر کس",
|
||||
"An error has occurred.": "خطایی رخ داده است.",
|
||||
"%(senderName)s answered the call.": "%(senderName)s تماس را پاسخ داد.",
|
||||
"A new password must be entered.": "گذواژه جدید باید وارد شود.",
|
||||
"Authentication": "احراز هویت",
|
||||
"Always show message timestamps": "همیشه مهر زمانهای پیام را نشان بده",
|
||||
"Advanced": "پیشرفته",
|
||||
"Camera": "دوربین",
|
||||
"Microphone": "میکروفون",
|
||||
"Default Device": "دستگاه پیشفرض",
|
||||
"No media permissions": "عدم مجوز رسانه",
|
||||
"No Webcams detected": "هیچ وبکمی شناسایی نشد",
|
||||
"No Microphones detected": "هیچ میکروفونی شناسایی نشد",
|
||||
"Admin": "ادمین",
|
||||
"Add": "افزودن",
|
||||
"Access Token:": "توکن دسترسی:",
|
||||
"Account": "حساب کابری",
|
||||
"Incorrect verification code": "کد فعالسازی اشتباه است",
|
||||
"Incorrect username and/or password.": "نام کاربری و یا گذرواژه اشتباه است.",
|
||||
"I have verified my email address": "ایمیل خود را تأید کردم",
|
||||
"Home": "خانه",
|
||||
"Hangup": "قطع",
|
||||
"For security, this session has been signed out. Please sign in again.": "برای امنیت، این نشست نامعتبر شده است. لطفاً دوباره وارد سیستم شوید."
|
||||
}
|
||||
|
|
|
@ -304,7 +304,7 @@
|
|||
"Unable to enable Notifications": "Ilmoitusten käyttöönotto epäonnistui",
|
||||
"Uploading %(filename)s and %(count)s others|other": "Ladataan %(filename)s ja %(count)s muuta",
|
||||
"Upload Failed": "Lataus epäonnistui",
|
||||
"Upload file": "Lataa tiedosto",
|
||||
"Upload file": "Lataa tiedostoja",
|
||||
"Upload new:": "Lataa uusi:",
|
||||
"Usage": "Käyttö",
|
||||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s vaihtoi aiheeksi \"%(topic)s\".",
|
||||
|
@ -626,7 +626,7 @@
|
|||
"Send Custom Event": "Lähetä mukautettu tapahtuma",
|
||||
"Advanced notification settings": "Lisäasetukset ilmoituksille",
|
||||
"Forget": "Unohda",
|
||||
"You cannot delete this image. (%(code)s)": "Et voi poistaa tätä kuvaa. (%(code)s)",
|
||||
"You cannot delete this image. (%(code)s)": "Et voi poistaa tätä kuvaa. (%(code)s)",
|
||||
"Cancel Sending": "Peruuta lähetys",
|
||||
"This Room": "Tämä huone",
|
||||
"Noisy": "Äänekäs",
|
||||
|
@ -702,7 +702,7 @@
|
|||
"Unable to join network": "Verkkoon liittyminen epäonnistui",
|
||||
"Sorry, your browser is <b>not</b> able to run %(brand)s.": "Valitettavasti %(brand)s <b>ei</b> toimi selaimessasi.",
|
||||
"Uploaded on %(date)s by %(user)s": "Ladattu %(date)s käyttäjän %(user)s toimesta",
|
||||
"Messages in group chats": "Viestit ryhmäkeskusteluissa",
|
||||
"Messages in group chats": "Viestit ryhmissä",
|
||||
"Yesterday": "Eilen",
|
||||
"Error encountered (%(errorDetail)s).": "Virhe: %(errorDetail)s.",
|
||||
"Low Priority": "Matala prioriteetti",
|
||||
|
@ -765,7 +765,7 @@
|
|||
"Show join/leave messages (invites/kicks/bans unaffected)": "Näytä liittymisten ja poistumisten viestit (ei vaikuta kutsuihin, huoneesta poistamisiin ja porttikieltoihin)",
|
||||
"Show developer tools": "Näytä kehitystyökalut",
|
||||
"Encrypted messages in one-to-one chats": "Salatut viestit kahdenkeskisissä keskusteluissa",
|
||||
"Encrypted messages in group chats": "Salatut viestit ryhmäkeskusteluissa",
|
||||
"Encrypted messages in group chats": "Salatut viestit ryhmissä",
|
||||
"The other party cancelled the verification.": "Toinen osapuoli perui varmennuksen.",
|
||||
"Verified!": "Varmennettu!",
|
||||
"You've successfully verified this user.": "Olet varmentanut tämän käyttäjän.",
|
||||
|
@ -798,7 +798,7 @@
|
|||
"Apple": "Omena",
|
||||
"Strawberry": "Mansikka",
|
||||
"Corn": "Maissi",
|
||||
"Pizza": "Pizza",
|
||||
"Pizza": "Pitsa",
|
||||
"Cake": "Kakku",
|
||||
"Heart": "Sydän",
|
||||
"Smiley": "Hymiö",
|
||||
|
@ -834,7 +834,7 @@
|
|||
"Pin": "Nuppineula",
|
||||
"Call in Progress": "Puhelu meneillään",
|
||||
"General": "Yleiset",
|
||||
"Security & Privacy": "Tietoturva ja -suoja",
|
||||
"Security & Privacy": "Tietoturva ja yksityisyys",
|
||||
"Roles & Permissions": "Roolit ja oikeudet",
|
||||
"Room Name": "Huoneen nimi",
|
||||
"Room Topic": "Huoneen aihe",
|
||||
|
@ -863,7 +863,7 @@
|
|||
"You don't currently have any stickerpacks enabled": "Sinulla ei ole tarrapaketteja käytössä",
|
||||
"Stickerpack": "Tarrapaketti",
|
||||
"Hide Stickers": "Piilota tarrat",
|
||||
"Show Stickers": "Näytä tarrat",
|
||||
"Show Stickers": "Tarrat",
|
||||
"Profile picture": "Profiilikuva",
|
||||
"Set a new account password...": "Aseta uusi salasana tilille...",
|
||||
"Set a new status...": "Aseta uusi tila...",
|
||||
|
@ -1135,7 +1135,7 @@
|
|||
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään, eikä kukaan voi rekisteröidä samaa käyttäjätunnusta. Tilisi poistuu kaikista huoneista, joihin se on liittynyt, ja tilisi tiedot poistetaan identiteettipalvelimeltasi. <b>Tämä toimenpidettä ei voi kumota.</b>",
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Tilisi poistaminen käytöstä <b>ei oletuksena saa meitä unohtamaan lähettämiäsi viestejä.</b> Jos haluaisit meidän unohtavan viestisi, rastita alla oleva ruutu.",
|
||||
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Viestien näkyvyys Matrixissa on samantapainen kuin sähköpostissa. Vaikka se, että unohdamme viestisi, tarkoittaa, ettei viestejäsi jaeta enää uusille tai rekisteröitymättömille käyttäjille, käyttäjät, jotka ovat jo saaneet viestisi pystyvät lukemaan jatkossakin omaa kopiotaan viesteistäsi.",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Unohda kaikki viestit, jotka olen lähettänyt, kun tilini on poistettu käytöstä (b>Varoitus:</b> tästä seuraa, että tulevat käyttäjät näkevät epätäydellisen version keskusteluista)",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Unohda kaikki viestit, jotka olen lähettänyt, kun tilini poistetaan käytöstä (<b>Varoitus:</b> tämä saa tulevat käyttäjät näkemään epätäydellisen version keskusteluista)",
|
||||
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Varmenna tämä käyttäjä merkitäksesi hänet luotetuksi. Käyttäjiin luottaminen antaa sinulle ylimääräistä mielenrauhaa käyttäessäsi osapuolten välistä salausta.",
|
||||
"Waiting for partner to confirm...": "Odotetaan, että toinen osapuoli varmistaa...",
|
||||
"Incoming Verification Request": "Saapuva varmennuspyyntö",
|
||||
|
@ -1631,7 +1631,7 @@
|
|||
"Discovery options will appear once you have added a phone number above.": "Etsinnän asetukset näkyvät sen jälkeen, kun olet lisännyt puhelinnumeron.",
|
||||
"Failed to connect to integration manager": "Yhdistäminen integraatioiden lähteeseen epäonnistui",
|
||||
"Trusted": "Luotettu",
|
||||
"Not trusted": "Epäluotettu",
|
||||
"Not trusted": "Ei-luotettu",
|
||||
"This client does not support end-to-end encryption.": "Tämä asiakasohjelma ei tue osapuolten välistä salausta.",
|
||||
"Messages in this room are not end-to-end encrypted.": "Tämän huoneen viestit eivät ole salattuja.",
|
||||
"Messages in this room are end-to-end encrypted.": "Tämän huoneen viestit ovat salattuja.",
|
||||
|
@ -1692,14 +1692,14 @@
|
|||
"Cross-signing and secret storage are enabled.": "Ristivarmennus ja salavarasto on käytössä.",
|
||||
"Cross-signing and secret storage are not yet set up.": "Ristivarmennusta ja salavarastoa ei ole vielä otettu käyttöön.",
|
||||
"Bootstrap cross-signing and secret storage": "Ota käyttöön ristivarmennus ja salavarasto",
|
||||
"Cross-signing public keys:": "Ristivarmennuksen julkiset avaimet:",
|
||||
"Cross-signing public keys:": "Ristiinvarmennuksen julkiset avaimet:",
|
||||
"not found": "ei löydetty",
|
||||
"Cross-signing private keys:": "Ristivarmennuksen salaiset avaimet:",
|
||||
"Cross-signing private keys:": "Ristiinvarmennuksen salaiset avaimet:",
|
||||
"in secret storage": "salavarastossa",
|
||||
"Secret storage public key:": "Salavaraston julkinen avain:",
|
||||
"in account data": "tunnuksen tiedoissa",
|
||||
"not stored": "ei tallennettu",
|
||||
"Cross-signing": "Ristivarmennus",
|
||||
"Cross-signing": "Ristiinvarmennus",
|
||||
"Backup has a <validity>valid</validity> signature from this user": "Varmuuskopiossa on <validity>kelvollinen</validity> allekirjoitus tältä käyttäjältä",
|
||||
"Backup has a <validity>invalid</validity> signature from this user": "Varmuuskopiossa on <validity>epäkelpo</validity> allekirjoitus tältä käyttäjältä",
|
||||
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "Varmuuskopiossa on <verify>tuntematon</verify> allekirjoitus käyttäjältä, jonka ID on %(deviceId)s",
|
||||
|
@ -1956,7 +1956,7 @@
|
|||
"Liberate your communication": "Vapauta viestintäsi",
|
||||
"Send a Direct Message": "Lähetä yksityisviesti",
|
||||
"Explore Public Rooms": "Selaa julkisia huoneita",
|
||||
"Create a Group Chat": "Luo ryhmäkeskustelu",
|
||||
"Create a Group Chat": "Luo ryhmä",
|
||||
"Super": "Super",
|
||||
"Cancel replying to a message": "Peruuta viestiin vastaaminen",
|
||||
"Jump to room search": "Siirry huonehakuun",
|
||||
|
@ -2019,10 +2019,10 @@
|
|||
"Verify all your sessions to ensure your account & messages are safe": "Varmenna kaikki istuntosi varmistaaksesi, että tunnuksesi ja viestisi ovat turvassa",
|
||||
"Verify the new login accessing your account: %(name)s": "Varmenna uusi tunnuksellesi sisäänkirjautunut taho: %(name)s",
|
||||
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Tällä hetkellä salasanan vaihtaminen nollaa kaikki osapuolten välisen salauksen avaimet kaikissa istunnoissa, tehden salatusta keskusteluhistoriasta lukukelvotonta, ellet ensin vie kaikkia huoneavaimiasi ja tuo niitä salasanan vaihtamisen jäkeen takaisin. Tulevaisuudessa tämä tulee toimimaan paremmin.",
|
||||
"Your homeserver does not support cross-signing.": "Kotipalvelimesi ei tue ristivarmennusta.",
|
||||
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Tunnuksellasi on ristivarmennuksen identiteetti salavarastossa, mutta tämä istunto ei luota siihen.",
|
||||
"Your homeserver does not support cross-signing.": "Kotipalvelimesi ei tue ristiinvarmennusta.",
|
||||
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Tunnuksellasi on ristiinvarmennuksen identiteetti salaisessa tallennustilassa, mutta tämä istunto ei vielä luota siihen.",
|
||||
"Reset cross-signing and secret storage": "Nollaa ristivarmennus ja salavarasto",
|
||||
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Varmenna jokainen käyttäjän istunto erikseen, äläkä luota ristivarmennettuihin laitteisiin.",
|
||||
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Varmenna jokainen istunto erikseen, äläkä luota ristiinvarmennettuihin laitteisiin.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results.": "Pidä salatut viestit turvallisessa välimuistissa, jotta ne näkyvät hakutuloksissa.",
|
||||
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)sissa ei ole joitain komponentteja, joita tarvitaan viestien turvalliseen välimuistitallennukseen. Jos haluat kokeilla tätä ominaisuutta, käännä mukautettu %(brand)s Desktop, jossa on mukana <nativeLink>hakukomponentit</nativeLink>.",
|
||||
"This session is backing up your keys. ": "Tämä istunto varmuuskopioi avaimesi. ",
|
||||
|
@ -2093,9 +2093,9 @@
|
|||
"Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Tämän huoneen viestit ovat salattuja osapuolten välisellä salauksella. Lue lisää ja varmenna tämä käyttäjä hänen profiilistaan.",
|
||||
"Enter the name of a new server you want to explore.": "Syötä sen uuden palvelimen nimi, jota hauat tutkia.",
|
||||
"%(networkName)s rooms": "Verkon %(networkName)s huoneet",
|
||||
"Destroy cross-signing keys?": "Tuhoa ristivarmennuksen avaimet?",
|
||||
"Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Ristivarmennuksen avainten tuhoamista ei voi kumota. Jokainen, jonka olet varmentanut, tulee näkemään turvallisuushälytyksiä. Et todennäköisesti halua tehdä tätä, ellet ole hukannut kaikkia laitteitasi, joista pystyt ristivarmentamaan.",
|
||||
"Clear cross-signing keys": "Tyhjennä ristivarmennuksen avaimet",
|
||||
"Destroy cross-signing keys?": "Tuhoa ristiinvarmennuksen avaimet?",
|
||||
"Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Ristiinvarmennuksen avainten tuhoamista ei voi kumota. Jokainen, jonka olet varmentanut, tulee näkemään turvallisuushälytyksiä. Et todennäköisesti halua tehdä tätä, ellet ole hukannut kaikkia laitteitasi, joista pystyit ristiinvarmentamaan.",
|
||||
"Clear cross-signing keys": "Tyhjennä ristiinvarmennuksen avaimet",
|
||||
"Enable end-to-end encryption": "Ota osapuolten välinen salaus käyttöön",
|
||||
"Session key": "Istunnon tunnus",
|
||||
"Verification Requests": "Varmennuspyynnöt",
|
||||
|
@ -2144,7 +2144,7 @@
|
|||
"Address (optional)": "Osoite (valinnainen)",
|
||||
"Light": "Vaalea",
|
||||
"Dark": "Tumma",
|
||||
"Emoji picker": "Emojivalitsin",
|
||||
"Emoji picker": "Emojit",
|
||||
"No recently visited rooms": "Ei hiljattain vierailtuja huoneita",
|
||||
"People": "Ihmiset",
|
||||
"Sort by": "Lajittelutapa",
|
||||
|
@ -2245,7 +2245,7 @@
|
|||
"Add widgets, bridges & bots": "Lisää sovelmia, siltoja ja botteja",
|
||||
"Edit widgets, bridges & bots": "Muokkaa sovelmia, siltoja ja botteja",
|
||||
"Widgets": "Sovelmat",
|
||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Yhteisöjen v2 prototyypit. Vaatii yhteensopivan kotipalvelimen. Erittäin kokeellinen — käytä varoen.",
|
||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Yhteisöjen v2 prototyypit. Vaatii yhteensopivan kotipalvelimen. Erittäin kokeellinen – käytä varoen.",
|
||||
"Change notification settings": "Muokkaa ilmoitusasetuksia",
|
||||
"The person who invited you already left the room, or their server is offline.": "Henkilö, joka kutsui sinut, on jo lähtenyt huoneesta, tai hänen palvelin on pois verkosta.",
|
||||
"The person who invited you already left the room.": "Henkilö, joka kutsui sinut, lähti jo huoneesta.",
|
||||
|
@ -2263,5 +2263,448 @@
|
|||
"Answered Elsewhere": "Vastattu muualla",
|
||||
"The call could not be established": "Puhelua ei voitu aloittaa",
|
||||
"The other party declined the call.": "Toinen osapuoli hylkäsi puhelun.",
|
||||
"Call Declined": "Puhelu hylätty"
|
||||
"Call Declined": "Puhelu hylätty",
|
||||
"%(brand)s Android": "%(brand)s Android",
|
||||
"%(brand)s iOS": "%(brand)s iOS",
|
||||
"Starting microphone...": "Käynnistetään mikrofonia...",
|
||||
"Starting camera...": "Käynnistetään kameraa...",
|
||||
"Call connecting...": "Yhdistetään puhelua...",
|
||||
"Calling...": "Soitetaan...",
|
||||
"%(creator)s created this DM.": "%(creator)s loi tämän yksityisviestin.",
|
||||
"You do not have permission to create rooms in this community.": "Sinulla ei ole lupaa luoda huoneita tähän yhteisöön.",
|
||||
"Cannot create rooms in this community": "Tähän yhteisöön ei voi luoda huoneita",
|
||||
"Welcome %(name)s": "Tervetuloa, %(name)s",
|
||||
"Create community": "Luo yhteisö",
|
||||
"No files visible in this room": "Tässä huoneessa ei näy tiedostoja",
|
||||
"Take a picture": "Ota kuva",
|
||||
"Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Palvelimesi ei vastaa joihinkin pyynnöistäsi. Alla on joitakin todennäköisimpiä syitä.",
|
||||
"Server isn't responding": "Palvelin ei vastaa",
|
||||
"Add comment": "Lisää kommentti",
|
||||
"Update community": "Päivitä yhteisö",
|
||||
"Create a room in %(communityName)s": "Luo huone yhteisöön %(communityName)s",
|
||||
"Your server requires encryption to be enabled in private rooms.": "Palvelimesi edellyttää, että salaus on käytössä yksityisissä huoneissa.",
|
||||
"An image will help people identify your community.": "Kuva auttaa ihmisiä tunnistamaan yhteisösi.",
|
||||
"Add image (optional)": "Lisää kuva (valinnainen)",
|
||||
"Enter name": "Syötä nimi",
|
||||
"What's the name of your community or team?": "Mikä on yhteisösi tai tiimisi nimi?",
|
||||
"Invite people to join %(communityName)s": "Kutsu ihmisiä yhteisöön %(communityName)s",
|
||||
"Send %(count)s invites|one": "Lähetä %(count)s kutsu",
|
||||
"Send %(count)s invites|other": "Lähetä %(count)s kutsua",
|
||||
"Add another email": "Lisää toinen sähköposti",
|
||||
"Click to view edits": "Napsauta nähdäksesi muokkaukset",
|
||||
"Edited at %(date)s": "Muokattu %(date)s",
|
||||
"Role": "Rooli",
|
||||
"Show files": "Näytä tiedostot",
|
||||
"%(count)s people|one": "%(count)s henkilö",
|
||||
"%(count)s people|other": "%(count)s ihmistä",
|
||||
"Forget Room": "Unohda huone",
|
||||
"Show previews of messages": "Näytä viestien esikatselut",
|
||||
"Show rooms with unread messages first": "Näytä ensimmäisenä huoneet, joissa on lukemattomia viestejä",
|
||||
"%(count)s results|one": "%(count)s tulos",
|
||||
"%(count)s results|other": "%(count)s tulosta",
|
||||
"Start a new chat": "Aloita uusi keskustelu",
|
||||
"Can't see what you’re looking for?": "Etkö löydä, mitä etsit?",
|
||||
"This is the start of <roomName/>.": "Tästä alkaa <roomName/>.",
|
||||
"%(displayName)s created this room.": "%(displayName)s loi tämän huoneen.",
|
||||
"You created this room.": "Loit tämän huoneen.",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Lisää aihe</a>, jotta ihmiset tietävät mistä on kyse.",
|
||||
"Topic: %(topic)s ": "Aihe: %(topic)s ",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Aihe: %(topic)s (<a>muokkaa</a>)",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "Tästä alkaa yksityisviestihistoriasi käyttäjän <displayName/> kanssa.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Vain te kaksi olette tässä keskustelussa, ellei jompi kumpi kutsu muita.",
|
||||
"Remove messages sent by others": "Poista toisten lähettämät viestit",
|
||||
"Privacy": "Tietosuoja",
|
||||
"not ready": "ei valmis",
|
||||
"ready": "valmis",
|
||||
"unexpected type": "odottamaton tyyppi",
|
||||
"Algorithm:": "Algoritmi:",
|
||||
"Failed to save your profile": "Profiilisi tallentaminen ei onnistunut",
|
||||
"Your server isn't responding to some <a>requests</a>.": "Palvelimesi ei vastaa joihinkin <a>pyyntöihin</a>.",
|
||||
"Incoming call": "Saapuva puhelu",
|
||||
"Incoming video call": "Saapuva videopuhelu",
|
||||
"Incoming voice call": "Saapuva äänipuhelu",
|
||||
"Unknown caller": "Tuntematon soittaja",
|
||||
"Enable experimental, compact IRC style layout": "Ota käyttöön kokeellinen, IRC-tyylinen asettelu",
|
||||
"Use Ctrl + Enter to send a message": "Ctrl + Enter lähettää viestin",
|
||||
"Use Command + Enter to send a message": "Komento + Enter lähettää viestin",
|
||||
"%(senderName)s ended the call": "%(senderName)s lopetti puhelun",
|
||||
"You ended the call": "Lopetit puhelun",
|
||||
"New version of %(brand)s is available": "%(brand)s-sovelluksesta on saatavilla uusi versio",
|
||||
"Update %(brand)s": "Päivitä %(brand)s",
|
||||
"Enable desktop notifications": "Ota työpöytäilmoitukset käyttöön",
|
||||
"Takes the call in the current room off hold": "Ottaa nykyisen huoneen puhelun pois pidosta",
|
||||
"Places the call in the current room on hold": "Asettaa nykyisen huoneen puhelun pitoon",
|
||||
"Away": "Poissa",
|
||||
"A confirmation email has been sent to %(emailAddress)s": "Vahvistussähköposti on lähetetty osoitteeseen %(emailAddress)s",
|
||||
"Open the link in the email to continue registration.": "Jatka rekisteröitymistä avaamalla sähköpostissa oleva linkki.",
|
||||
"Enter email address": "Syötä sähköpostiosoite",
|
||||
"Enter phone number": "Syötä puhelinnumero",
|
||||
"Now, let's help you get started": "Autetaanpa sinut alkuun",
|
||||
"delete the address.": "poista osoite.",
|
||||
"Filter rooms and people": "Suodata huoneita ja ihmisiä",
|
||||
"Go to Home View": "Siirry kotinäkymään",
|
||||
"Community and user menu": "Yhteisö- ja käyttäjävalikko",
|
||||
"Decline All": "Kieltäydy kaikista",
|
||||
"Approve": "Hyväksy",
|
||||
"Your area is experiencing difficulties connecting to the internet.": "Alueellasi on ongelmia internet-yhteyksissä.",
|
||||
"The server is offline.": "Palvelin ei ole verkossa.",
|
||||
"Your firewall or anti-virus is blocking the request.": "Palomuurisi tai virustentorjuntaohjelmasi estää pyynnön.",
|
||||
"The server (%(serverName)s) took too long to respond.": "Palvelin (%(serverName)s) ei vastannut ajoissa.",
|
||||
"Feedback sent": "Palaute lähetetty",
|
||||
"There was an error updating your community. The server is unable to process your request.": "Yhteisösi päivittämisessä tapahtui virhe. Palvelin ei voi käsitellä pyyntöäsi.",
|
||||
"You can change this later if needed.": "Voit tarvittaessa vaihtaa tämän myöhemmin.",
|
||||
"There was an error creating your community. The name may be taken or the server is unable to process your request.": "Yhteisösi luomisessa tapahtui virhe. Nimi saattaa olla varattu tai palvelin ei voi käsitellä pyyntöäsi.",
|
||||
"Download logs": "Lataa lokit",
|
||||
"Preparing to download logs": "Valmistellaan lokien lataamista",
|
||||
"About": "Tietoa",
|
||||
"Unpin": "Poista kiinnitys",
|
||||
"Customise your appearance": "Mukauta ulkoasuasi",
|
||||
"Appearance Settings only affect this %(brand)s session.": "Ulkoasuasetukset vaikuttavat vain tähän %(brand)s-istuntoon.",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Aseta käyttöjärjestelmääsi asennetun fontin nimi, niin %(brand)s pyrkii käyttämään sitä.",
|
||||
"The operation could not be completed": "Toimintoa ei voitu tehdä loppuun asti",
|
||||
"There are advanced notifications which are not shown here.": "On edistyneitä ilmoituksia, joita ei näytetä tässä.",
|
||||
"Return to call": "Palaa puheluun",
|
||||
"Voice Call": "Äänipuhelu",
|
||||
"Video Call": "Videopuhelu",
|
||||
"Send stickers to your active room as you": "Lähetä aktiiviseen huoneeseesi tarroja itsenäsi",
|
||||
"Send stickers to this room as you": "Lähetä tähän huoneeseen tarroja itsenäsi",
|
||||
"Change the avatar of your active room": "Vaihda aktiivisen huoneesi kuva",
|
||||
"Change the avatar of this room": "Vaihda huoneen kuva",
|
||||
"Change the name of your active room": "Muuta aktiivisen huoneesi nimeä",
|
||||
"Change the name of this room": "Muuta tämän huoneen nimeä",
|
||||
"Change the topic of your active room": "Muuta aktiivisen huoneesi aihetta",
|
||||
"Change the topic of this room": "Muuta huoneen aihetta",
|
||||
"Change which room you're viewing": "Vaihda näytettävää huonetta",
|
||||
"Send stickers into your active room": "Lähetä tarroja aktiiviseen huoneeseesi",
|
||||
"Send stickers into this room": "Lähetä tarroja tähän huoneeseen",
|
||||
"Comoros": "Komorit",
|
||||
"Colombia": "Kolumbia",
|
||||
"Cocos (Keeling) Islands": "Kookossaaret",
|
||||
"Christmas Island": "Joulusaari",
|
||||
"China": "Kiina",
|
||||
"Chile": "Chile",
|
||||
"Chad": "Tšad",
|
||||
"Central African Republic": "Keski-Afrikan tasavalta",
|
||||
"Cayman Islands": "Caymansaaret",
|
||||
"Caribbean Netherlands": "Alankomaiden Karibia",
|
||||
"Cape Verde": "Kap Verde",
|
||||
"Canada": "Kanada",
|
||||
"Cameroon": "Kamerun",
|
||||
"Cambodia": "Kambodža",
|
||||
"Burundi": "Burundi",
|
||||
"Burkina Faso": "Burkina Faso",
|
||||
"Bulgaria": "Bulgaria",
|
||||
"Brunei": "Brunei",
|
||||
"British Virgin Islands": "Brittiläiset Neitsytsaaret",
|
||||
"British Indian Ocean Territory": "Brittiläinen Intian valtameren alue",
|
||||
"Brazil": "Brasilia",
|
||||
"Bouvet Island": "Bouvet’nsaari",
|
||||
"Botswana": "Botswana",
|
||||
"Bosnia": "Bosnia ja Hertsegovina",
|
||||
"Bolivia": "Bolivia",
|
||||
"Bhutan": "Bhutan",
|
||||
"Bermuda": "Bermuda",
|
||||
"Benin": "Benin",
|
||||
"Belize": "Belize",
|
||||
"Belgium": "Belgia",
|
||||
"Belarus": "Valko-Venäjä",
|
||||
"Barbados": "Barbados",
|
||||
"Bangladesh": "Bangladesh",
|
||||
"Bahrain": "Bahrain",
|
||||
"Bahamas": "Bahama",
|
||||
"Azerbaijan": "Azerbaidžan",
|
||||
"Austria": "Itävalta",
|
||||
"Australia": "Australia",
|
||||
"Aruba": "Aruba",
|
||||
"Armenia": "Armenia",
|
||||
"Argentina": "Argentiina",
|
||||
"Antigua & Barbuda": "Antigua ja Barbuda",
|
||||
"Antarctica": "Antarktis",
|
||||
"Anguilla": "Anguilla",
|
||||
"Angola": "Angola",
|
||||
"Andorra": "Andorra",
|
||||
"American Samoa": "Amerikan Samoa",
|
||||
"Algeria": "Algeria",
|
||||
"Albania": "Albania",
|
||||
"Åland Islands": "Ahvenanmaa",
|
||||
"Afghanistan": "Afganistan",
|
||||
"United States": "Yhdysvallat",
|
||||
"United Kingdom": "Iso-Britannia",
|
||||
"No other application is using the webcam": "Mikään muu sovellus ei käytä kameraa",
|
||||
"Permission is granted to use the webcam": "Lupa käyttää kameraa myönnetty",
|
||||
"A microphone and webcam are plugged in and set up correctly": "Mikrofoni ja kamera on kytketty ja asennettu oiken",
|
||||
"Call failed because webcam or microphone could not be accessed. Check that:": "Puhelu epäonnistui, koska kameraa tai mikrofonia ei voitu käyttää. Tarkista että:",
|
||||
"Unable to access webcam / microphone": "Kameraa / mikrofonia ei voi käyttää",
|
||||
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Puhelu epäonnistui, koska mikrofonia ei voitu käyttää. Tarkista, että mikrofoni on kytketty ja asennettu oikein.",
|
||||
"Unable to access microphone": "Mikrofonia ei voi käyttää",
|
||||
"El Salvador": "El Salvador",
|
||||
"Egypt": "Egypti",
|
||||
"Ecuador": "Ecuador",
|
||||
"Dominican Republic": "Dominikaaninen tasavalta",
|
||||
"Dominica": "Dominica",
|
||||
"Djibouti": "Djibouti",
|
||||
"Denmark": "Tanska",
|
||||
"Côte d’Ivoire": "Norsunluurannikko",
|
||||
"Czech Republic": "Tšekki",
|
||||
"Cyprus": "Kypros",
|
||||
"Curaçao": "Curaçao",
|
||||
"Cuba": "Kuuba",
|
||||
"Croatia": "Kroatia",
|
||||
"Costa Rica": "Costa Rica",
|
||||
"Cook Islands": "Cookinsaaret",
|
||||
"Congo - Kinshasa": "Kongo-Kinshasa",
|
||||
"Congo - Brazzaville": "Kongo-Brazzavile",
|
||||
"Micronesia": "Mikronesia",
|
||||
"Mexico": "Meksiko",
|
||||
"Mayotte": "Mayotte",
|
||||
"Mauritius": "Mauritius",
|
||||
"Mauritania": "Mauritania",
|
||||
"Martinique": "Martinique",
|
||||
"Marshall Islands": "Marshallinsaaret",
|
||||
"Malta": "Malta",
|
||||
"Mali": "Mali",
|
||||
"Maldives": "Malediivit",
|
||||
"Malaysia": "Malesia",
|
||||
"Malawi": "Malawi",
|
||||
"Madagascar": "Madagaskar",
|
||||
"Macedonia": "Pohjois-Makedonia",
|
||||
"Macau": "Macao",
|
||||
"Luxembourg": "Luxemburg",
|
||||
"Lithuania": "Liettua",
|
||||
"Liechtenstein": "Liechtenstein",
|
||||
"Libya": "Libya",
|
||||
"Liberia": "Liberia",
|
||||
"Lesotho": "Lesotho",
|
||||
"Lebanon": "Libanon",
|
||||
"Latvia": "Latvia",
|
||||
"Laos": "Laos",
|
||||
"Kyrgyzstan": "Kirgisia",
|
||||
"Kuwait": "Kuwait",
|
||||
"Kosovo": "Kosovo",
|
||||
"Kiribati": "Kiribati",
|
||||
"Kenya": "Kenia",
|
||||
"Kazakhstan": "Kazakstan",
|
||||
"Jordan": "Jordania",
|
||||
"Jersey": "Jersey",
|
||||
"Japan": "Japani",
|
||||
"Jamaica": "Jamaika",
|
||||
"Italy": "Italia",
|
||||
"Israel": "Israel",
|
||||
"Isle of Man": "Mansaari",
|
||||
"Ireland": "Irlanti",
|
||||
"Iraq": "Irak",
|
||||
"Iran": "Iran",
|
||||
"Indonesia": "Indonesia",
|
||||
"India": "Intia",
|
||||
"Iceland": "Islanti",
|
||||
"Hungary": "Unkari",
|
||||
"Hong Kong": "Hongkong",
|
||||
"Honduras": "Honduras",
|
||||
"Heard & McDonald Islands": "Heard ja McDonaldinsaaret",
|
||||
"Haiti": "Haiti",
|
||||
"Guyana": "Guyana",
|
||||
"Guinea-Bissau": "Guinea-Bissau",
|
||||
"Guinea": "Guinea",
|
||||
"Guernsey": "Guernsey",
|
||||
"Guatemala": "Guatemala",
|
||||
"Guam": "Guam",
|
||||
"Guadeloupe": "Guadeloupe",
|
||||
"Grenada": "Grenada",
|
||||
"Greenland": "Grönlanti",
|
||||
"Greece": "Kreikka",
|
||||
"Gibraltar": "Gibraltar",
|
||||
"Ghana": "Ghana",
|
||||
"Germany": "Saksa",
|
||||
"Georgia": "Georgia",
|
||||
"Gambia": "Gambia",
|
||||
"Gabon": "Gabon",
|
||||
"French Southern Territories": "Ranskan eteläiset ja antarktiset alueet",
|
||||
"French Polynesia": "Ranskan Polynesia",
|
||||
"French Guiana": "Ranskan Guayana",
|
||||
"France": "Ranska",
|
||||
"Finland": "Suomi",
|
||||
"Fiji": "Fidži",
|
||||
"Faroe Islands": "Färsaaret",
|
||||
"Falkland Islands": "Falklandinsaaret",
|
||||
"Ethiopia": "Etiopia",
|
||||
"Estonia": "Viro",
|
||||
"Eritrea": "Eritrea",
|
||||
"Equatorial Guinea": "Päiväntasaajan Guinea",
|
||||
"You've reached the maximum number of simultaneous calls.": "Saavutit samanaikaisten puheluiden enimmäismäärän.",
|
||||
"Too Many Calls": "Liian monta puhelua",
|
||||
"Moldova": "Moldova",
|
||||
"You’re all caught up": "Olet ajan tasalla",
|
||||
"Room settings": "Huoneen asetukset",
|
||||
"or another cross-signing capable Matrix client": "tai muu ristiinvarmentava Matrix-asiakas",
|
||||
"a device cross-signing signature": "laitteen ristiinvarmennuksen allekirjoitus",
|
||||
"a new cross-signing key signature": "Uusi ristiinvarmennuksen allekirjoitus",
|
||||
"Cross-signing is not set up.": "Ristiinvarmennusta ei ole asennettu.",
|
||||
"Cross-signing is ready for use.": "Ristiinvarmennus on käyttövalmis.",
|
||||
"Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Varmuuskopioi tilitietojesi salausavaimet, istuntojen menettämisen varalta. Avaimet varmistetaan ainutlaatuisella Palatusavaimella.",
|
||||
"well formed": "hyvin muotoiltu",
|
||||
"Backup version:": "Varmuuskopiointiversio:",
|
||||
"Backup key stored:": "Varmuuskopioavain tallennettu:",
|
||||
"Backup key cached:": "Välimuistissa oleva varmuuskopioavain:",
|
||||
"Secret storage:": "Salainen tallennus:",
|
||||
"Hey you. You're the best!": "Hei siellä, olet paras!",
|
||||
"Room ID or address of ban list": "Huonetunnus tai -osoite on estolistalla",
|
||||
"Secure Backup": "Turvallinen varmuuskopio",
|
||||
"Add a photo, so people can easily spot your room.": "Lisää kuva, jotta ihmiset voivat helpommin huomata huoneesi.",
|
||||
"Hide Widgets": "Piilota sovelmat",
|
||||
"Show Widgets": "Näytä sovelmat",
|
||||
"Explore community rooms": "Selaa yhteisön huoneita",
|
||||
"Explore public rooms": "Selaa julkisia huoneita",
|
||||
"Custom Tag": "Mukautettu tunniste",
|
||||
"Explore all public rooms": "Selaa julkisia huoneita",
|
||||
"List options": "Lajittele",
|
||||
"Activity": "Aktiivisuus",
|
||||
"A-Z": "A-Ö",
|
||||
"Server Options": "Palvelimen asetukset",
|
||||
"Information": "Tiedot",
|
||||
"Effects": "Tehosteet",
|
||||
"Zimbabwe": "Zimbabwe",
|
||||
"Zambia": "Sambia",
|
||||
"Yemen": "Jemen",
|
||||
"Western Sahara": "Länsi-Sahara",
|
||||
"Wallis & Futuna": "Wallis ja Futuna",
|
||||
"Vietnam": "Vietnam",
|
||||
"Venezuela": "Venezuela",
|
||||
"Vatican City": "Vatikaani",
|
||||
"Vanuatu": "Vanuatu",
|
||||
"Uzbekistan": "Uzbekistan",
|
||||
"Uruguay": "Uruguay",
|
||||
"United Arab Emirates": "Yhdistyneet arabiemiirikunnat",
|
||||
"Ukraine": "Ukraina",
|
||||
"Uganda": "Uganda",
|
||||
"U.S. Virgin Islands": "Yhdysvaltain Neitsytsaaret",
|
||||
"Tuvalu": "Tuvalu",
|
||||
"Turks & Caicos Islands": "Turks- ja Caicossaaret",
|
||||
"Turkmenistan": "Turkmenistan",
|
||||
"Turkey": "Turkki",
|
||||
"Tunisia": "Tunisia",
|
||||
"Trinidad & Tobago": "Trinidad ja Tobago",
|
||||
"Tonga": "Tonga",
|
||||
"Tokelau": "Tokelau",
|
||||
"Togo": "Togo",
|
||||
"Timor-Leste": "Itä-Timor",
|
||||
"Thailand": "Thaimaa",
|
||||
"Tanzania": "Tansania",
|
||||
"Tajikistan": "Tadžikistan",
|
||||
"Taiwan": "Taiwan",
|
||||
"São Tomé & Príncipe": "São Tomé ja Príncipe",
|
||||
"Syria": "Syyria",
|
||||
"Switzerland": "Sveitsi",
|
||||
"Sweden": "Ruotsi",
|
||||
"Swaziland": "Swazimaa",
|
||||
"Svalbard & Jan Mayen": "Huippuvuoret ja Jan Mayen",
|
||||
"Suriname": "Suriname",
|
||||
"Sudan": "Sudan",
|
||||
"St. Vincent & Grenadines": "Saint Vincent ja Grenadiinit",
|
||||
"St. Pierre & Miquelon": "Saint-Pierre ja Miquelon",
|
||||
"St. Martin": "Saint-Martin",
|
||||
"St. Lucia": "Saint Lucia",
|
||||
"St. Kitts & Nevis": "Saint Kitts ja Nevis",
|
||||
"St. Helena": "Saint Helena",
|
||||
"St. Barthélemy": "Saint-Barthélemy",
|
||||
"Sri Lanka": "Sri Lanka",
|
||||
"Spain": "Espanja",
|
||||
"South Sudan": "Etelä-Sudan",
|
||||
"South Korea": "Etelä-Korea",
|
||||
"South Georgia & South Sandwich Islands": "Etelä-Georgia ja Eteläiset Sandwichsaaret",
|
||||
"South Africa": "Etelä-Afrikka",
|
||||
"Somalia": "Somalia",
|
||||
"Solomon Islands": "Salomonsaaret",
|
||||
"Slovenia": "Slovenia",
|
||||
"Slovakia": "Slovakia",
|
||||
"Sint Maarten": "Sint Maarten",
|
||||
"Singapore": "Singapore",
|
||||
"Sierra Leone": "Sierra Leone",
|
||||
"Seychelles": "Seychellit",
|
||||
"Serbia": "Serbia",
|
||||
"Senegal": "Senegal",
|
||||
"Saudi Arabia": "Saudi-Arabia",
|
||||
"San Marino": "San Marino",
|
||||
"Samoa": "Samoa",
|
||||
"Réunion": "Réunion",
|
||||
"Rwanda": "Ruanda",
|
||||
"Russia": "Venäjä",
|
||||
"Romania": "Romania",
|
||||
"Qatar": "Qatar",
|
||||
"Puerto Rico": "Puerto Rico",
|
||||
"Portugal": "Portugali",
|
||||
"Poland": "Puola",
|
||||
"Pitcairn Islands": "Pitcairnsaaret",
|
||||
"Philippines": "Filippiinit",
|
||||
"Peru": "Peru",
|
||||
"Paraguay": "Paraguay",
|
||||
"Papua New Guinea": "Papua-Uusi-Guinea",
|
||||
"Panama": "Panama",
|
||||
"Palestine": "Palestiina",
|
||||
"Palau": "Palau",
|
||||
"Pakistan": "Pakistan",
|
||||
"Oman": "Oman",
|
||||
"Norway": "Norja",
|
||||
"Northern Mariana Islands": "Pohjois-Mariaanit",
|
||||
"North Korea": "Pohjois-Korea",
|
||||
"Norfolk Island": "Norfolkinsaari",
|
||||
"Niue": "Niue",
|
||||
"Nigeria": "Nigeria",
|
||||
"Niger": "Niger",
|
||||
"Nicaragua": "Nicaragua",
|
||||
"New Zealand": "Uusi-Seelanti",
|
||||
"New Caledonia": "Uusi-Kaledonia",
|
||||
"Netherlands": "Alankomaat",
|
||||
"Nepal": "Nepal",
|
||||
"Nauru": "Nauru",
|
||||
"Namibia": "Namibia",
|
||||
"Myanmar": "Myanmar",
|
||||
"Mozambique": "Mosambik",
|
||||
"Morocco": "Marokko",
|
||||
"Montserrat": "Montserrat",
|
||||
"Montenegro": "Montenegro",
|
||||
"Mongolia": "Mongolia",
|
||||
"Monaco": "Monaco",
|
||||
"Room Info": "Huoneen tiedot",
|
||||
"not found in storage": "ei löytynyt muistista",
|
||||
"Sign into your homeserver": "Kirjaudu sisään kotipalvelimellesi",
|
||||
"About homeservers": "Tietoa kotipalvelimista",
|
||||
"Not encrypted": "Ei salattu",
|
||||
"You have no visible notifications in this room.": "Olet nähnyt tämän huoneen kaikki ilmoitukset.",
|
||||
"Session verified": "Istunto vahvistettu",
|
||||
"Verify this login": "Vahvista tämä sisäänkirjautuminen",
|
||||
"User settings": "Käyttäjäasetukset",
|
||||
"Community settings": "Yhteisön asetukset",
|
||||
"Got an account? <a>Sign in</a>": "Sinulla on jo tili? <a>Kirjaudu sisään</a>",
|
||||
"New here? <a>Create an account</a>": "Uusi täällä? <a>Luo tili</a>",
|
||||
"Failed to find the general chat for this community": "Tämän yhteisön yleisen keskustelun löytäminen epäonnistui",
|
||||
"Explore rooms in %(communityName)s": "Tutki %(communityName)s -yhteisön huoneita",
|
||||
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Poistetaanko huoneosoite %(alias)s ja %(name)s hakemisto?",
|
||||
"Self-verification request": "Itsevarmennuspyyntö",
|
||||
"Add a photo so people know it's you.": "Lisää kuva, jotta ihmiset tietävät, että se olet sinä.",
|
||||
"Great, that'll help people know it's you": "Hienoa, tämä auttaa ihmisiä tietämään, että se olet sinä",
|
||||
"Attach files from chat or just drag and drop them anywhere in a room.": "Liitä tiedostot keskustelusta tai vedä ja pudota ne mihin tahansa huoneeseen.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Käytä sähköpostiosoitetta tai puhelinnumeroa, jos haluat olla löydettävissä nykyisille yhteystiedoille.",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Käytä sähköpostiosoitetta, jos haluat olla löydettävissä nykyisille yhteystiedoille.",
|
||||
"Add an email to be able to reset your password.": "Lisää sähköpostiosoite, jotta voit nollata salasanasi.",
|
||||
"Forgot password?": "Unohtuiko salasana?",
|
||||
"That phone number doesn't look quite right, please check and try again": "Tämä puhelinnumero ei näytä oikealta, tarkista se ja yritä uudelleen",
|
||||
"Move right": "Siirry oikealle",
|
||||
"Move left": "Siirry vasemmalle",
|
||||
"Revoke permissions": "Peruuta käyttöoikeudet",
|
||||
"Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Varoitus: Henkilökohtaiset tietosi (mukaan lukien salausavaimet) tallennetaan edelleen tähän istuntoon. Poista ne jos lopetat istunnon käytön, tai haluat kirjautua toiselle tilille.",
|
||||
"You're all caught up.": "Olet ajan tasalla.",
|
||||
"Unable to validate homeserver": "Kotipalvelimen vahvistus epäonnistui",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Tämä %(brand)s-versio ei tue salattujen viestien hakua",
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Tämä %(brand)s-versio ei tue joidenkin salattujen tiedostojen katselua",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Voit tarkastella kaikkia salattuja tiedostoja <a>työpöytäsovelluksella</a>",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "Käytä salattuja viestejä <a>työpöytäsovelluksella</a>",
|
||||
"Ignored attempt to disable encryption": "Ohitettu yritys poistaa salaus käytöstä",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Tässä olevat viestit on päästä-päähän -salattu. Vahvista %(displayName)s heidät - napauta heidän profiilikuvia.",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Tämän huoneen viestit on päästä-päähän -salattu. Kun ihmisiä liittyy, voit vahvistaa heidät heidän profiilista, napauta vain heidän profiilikuvaa.",
|
||||
"Unpin a widget to view it in this panel": "Irrota sovelma, jotta voit tarkastella sitä tässä paneelissa",
|
||||
"You can only pin up to %(count)s widgets|other": "Voit kiinnittää enintään %(count)s sovelmaa",
|
||||
"Favourited": "Suositut",
|
||||
"Use the + to make a new room or explore existing ones below": "Luo uusi huone tai tutustu alla oleviin + -painikeella",
|
||||
"%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s asetti palvelimen pääsyhallintaluetteloon tämän huoneen.",
|
||||
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s muutti palvelimen pääsyhallintaluetteloa tälle huoneelle."
|
||||
}
|
||||
|
|
|
@ -2503,7 +2503,7 @@
|
|||
"No files visible in this room": "Aucun fichier visible dans ce salon",
|
||||
"Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of <a>element.io</a>.": "Entrez l'emplacement de votre serveur d'accueil Element Matrix Services. Cela peut utiliser votre propre nom de domaine ou être un sous-domaine de <a>element.io</a>.",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Vous pouvez utiliser l'option de serveur personnalisé pour vous connecter à d'autres serveurs Matrix en spécifiant une URL de serveur d'accueil différente. Cela vous permet d'utiliser %(brand)s avec un compte Matrix existant sur un serveur d'accueil différent.",
|
||||
"Away": "Tout droit",
|
||||
"Away": "Absent",
|
||||
"Move right": "Aller à droite",
|
||||
"Move left": "Aller à gauche",
|
||||
"Revoke permissions": "Révoquer les permissions",
|
||||
|
|
|
@ -1580,7 +1580,7 @@
|
|||
"Dark": "Escuro",
|
||||
"Customise your appearance": "Personaliza o aspecto",
|
||||
"Appearance Settings only affect this %(brand)s session.": "Os axustes da aparencia só lle afectan a esta sesión %(brand)s.",
|
||||
"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.": "As solicitudes de compartir Chave envíanse ás outras túas sesións abertas. Se rexeitaches ou obviaches a solicitude nas outras sesións, preme aquí para voltar a facer a solicitude.",
|
||||
"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.": "As solicitudes de compartir Chave envíanse ás outras túas sesións abertas. Se rexeitaches ou obviaches a solicitude nas outras sesións, preme aquí para volver a facer a solicitude.",
|
||||
"If your other sessions do not have the key for this message you will not be able to decrypt them.": "Se as túas outras sesións non teñen a chave para esta mensaxe non poderás descifrala.",
|
||||
"<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Volta a solicitar chaves de cifrado</requestLink> desde as outras sesións.",
|
||||
"This message cannot be decrypted": "Esta mensaxe non pode descifrarse",
|
||||
|
@ -1713,7 +1713,7 @@
|
|||
"Remove recent messages": "Eliminar mensaxes recentes",
|
||||
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> en %(roomName)s",
|
||||
"Deactivate user?": "¿Desactivar usuaria?",
|
||||
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Ao desactivar esta usuaria ficará desconectada e non poderá voltar a conectar. Ademáis deixará todas as salas nas que estivese. Esta acción non ten volta, ¿desexas desactivar esta usuaria?",
|
||||
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Ao desactivar esta usuaria ficará desconectada e non poderá volver a conectar. Ademáis deixará todas as salas nas que estivese. Esta acción non ten volta, ¿desexas desactivar esta usuaria?",
|
||||
"Deactivate user": "Desactivar usuaria",
|
||||
"Failed to deactivate user": "Fallo ao desactivar a usuaria",
|
||||
"This client does not support end-to-end encryption.": "Este cliente non soporta o cifrado extremo-a-extremo.",
|
||||
|
@ -1864,8 +1864,8 @@
|
|||
"Hide advanced": "Ocultar Avanzado",
|
||||
"Show advanced": "Mostrar Avanzado",
|
||||
"Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Evitar que usuarias de outros servidores matrix se unan a esta sala (Este axuste non se pode cambiar máis tarde!)",
|
||||
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Para evitar perder o historial da conversa, debes exportar as chaves da sala antes de desconectarte. Necesitarás voltar á nova versión de %(brand)s para facer esto",
|
||||
"You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Xa utilizaches unha versión máis nova de %(brand)s nesta sesión. Para usar esta versión novamente con cifrado extremo-a-extremo tes que desconectarte e voltar a conectar.",
|
||||
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Para evitar perder o historial da conversa, debes exportar as chaves da sala antes de desconectarte. Necesitarás volver á nova versión de %(brand)s para facer esto",
|
||||
"You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Xa utilizaches unha versión máis nova de %(brand)s nesta sesión. Para usar esta versión novamente con cifrado extremo-a-extremo tes que desconectarte e volver a conectar.",
|
||||
"Incompatible Database": "Base de datos non compatible",
|
||||
"Continue With Encryption Disabled": "Continuar con Cifrado Desactivado",
|
||||
"Are you sure you want to deactivate your account? This is irreversible.": "¿Tes a certeza de querer desactivar a túa conta? Esto é irreversible.",
|
||||
|
@ -1918,7 +1918,7 @@
|
|||
"The authenticity of this encrypted message can't be guaranteed on this device.": "A autenticidade desta mensaxe cifrada non está garantida neste dispositivo.",
|
||||
"Signature upload success": "Subeuse correctamente a sinatura",
|
||||
"Signature upload failed": "Fallou a subida da sinatura",
|
||||
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "Anteriormente utilizaches %(brand)s en %(host)s con carga preguiceira de membros. Nesta versión a carga preguiceira está desactivada. Como a caché local non é compatible entre as dúas configuracións, %(brand)s precisa voltar a sincronizar a conta.",
|
||||
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "Anteriormente utilizaches %(brand)s en %(host)s con carga preguiceira de membros. Nesta versión a carga preguiceira está desactivada. Como a caché local non é compatible entre as dúas configuracións, %(brand)s precisa volver a sincronizar a conta.",
|
||||
"Incompatible local cache": "Caché local incompatible",
|
||||
"Clear cache and resync": "Baleirar caché e sincronizar",
|
||||
"%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s utiliza agora entre 3 e 5 veces menos memoria, cargando só información sobre as usuarias cando é preciso. Agarda mentras se sincroniza co servidor!",
|
||||
|
@ -2079,7 +2079,7 @@
|
|||
"Premium hosting for organisations <a>Learn more</a>": "Hospedaxe Premium para organizacións <a>Saber máis</a>",
|
||||
"Find other public servers or use a custom server": "Atopa outros servidores públicos ou usa un servidor personalizado",
|
||||
"Couldn't load page": "Non se puido cargar a páxina",
|
||||
"You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Administras esta comunidade. Non poderás voltar a unirte sen un convite doutra persoa administradora.",
|
||||
"You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Administras esta comunidade. Non poderás volver a unirte sen un convite doutra persoa administradora.",
|
||||
"Want more than a community? <a>Get your own server</a>": "¿Queres algo máis que unha comunidade? <a>Monta o teu propio servidor</a>",
|
||||
"This homeserver does not support communities": "Este servidor non soporta comunidades",
|
||||
"Welcome to %(appName)s": "Benvida a %(appName)s",
|
||||
|
@ -2845,5 +2845,131 @@
|
|||
"Filter rooms and people": "Fitrar salas e persoas",
|
||||
"Open the link in the email to continue registration.": "Abre a ligazón que hai no email para continuar co rexistro.",
|
||||
"A confirmation email has been sent to %(emailAddress)s": "Enviouse un email de confirmación a %(emailAddress)s",
|
||||
"Start a new chat": "Comezar nova conversa"
|
||||
"Start a new chat": "Comezar nova conversa",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas.",
|
||||
"Go to Home View": "Ir á Páxina de Inicio",
|
||||
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML para a páxina da túa comunidade</h1>\n<p>\n Usa a descrición longa para presentar a comunidade ás novas particpantes, ou publicar \nalgunha <a href=\"foo\">ligazón</a> importante\n \n</p>\n<p>\n Tamén podes engadir imaxes con URLs de Matrix <img src=\"mxc://url\" />\n</p>\n",
|
||||
"The <b>%(capability)s</b> capability": "A capacidade de <b>%(capability)s</b>",
|
||||
"Decline All": "Rexeitar todo",
|
||||
"Approve": "Aprobar",
|
||||
"This widget would like to:": "O widget podería querer:",
|
||||
"Approve widget permissions": "Aprovar permisos do widget",
|
||||
"Use Ctrl + Enter to send a message": "Usar Ctrl + Enter para enviar unha mensaxe",
|
||||
"Use Command + Enter to send a message": "Usar Command + Enter para enviar unha mensaxe",
|
||||
"See <b>%(msgtype)s</b> messages posted to your active room": "Ver mensaxes <b>%(msgtype)s</b> publicados na túa sala activa",
|
||||
"See <b>%(msgtype)s</b> messages posted to this room": "Ver mensaxes <b>%(msgtype)s</b> publicados nesta sala",
|
||||
"Send <b>%(msgtype)s</b> messages as you in your active room": "Enviar mensaxes <b>%(msgtype)s</b> no teu nome á túa sala activa",
|
||||
"Send <b>%(msgtype)s</b> messages as you in this room": "Enviar mensaxes <b>%(msgtype)s</b> no teu nome a esta sala",
|
||||
"See general files posted to your active room": "Ver ficheiros publicados na túa sala activa",
|
||||
"See general files posted to this room": "Ver ficheiros publicados nesta sala",
|
||||
"Send general files as you in your active room": "Enviar ficheiros no teu nome á túa sala activa",
|
||||
"Send general files as you in this room": "Enviar ficheiros no teu nome a esta sala",
|
||||
"See videos posted to your active room": "Ver vídeos publicados na túa sala activa",
|
||||
"See videos posted to this room": "Ver vídeos publicados nesta sala",
|
||||
"Send videos as you in your active room": "Enviar vídeos no teu nome á túa sala activa",
|
||||
"Send videos as you in this room": "Enviar vídeos no teu nome a esta sala",
|
||||
"See images posted to your active room": "Ver imaxes publicadas na túa sala activa",
|
||||
"See images posted to this room": "Ver imaxes publicadas nesta sala",
|
||||
"Send images as you in your active room": "Enviar imaxes no teu nome á túa sala activa",
|
||||
"Send images as you in this room": "Enviar imaxes no teu nome a esta sala",
|
||||
"See emotes posted to your active room": "Ver emotes publicados na túa sala activa",
|
||||
"See emotes posted to this room": "Ver emotes publicados nesta sala",
|
||||
"Send emotes as you in your active room": "Enviar emotes no teu nome á túa sala activa",
|
||||
"Send emotes as you in this room": "Enviar emotes no teu nome a esta sala",
|
||||
"See text messages posted to your active room": "Ver mensaxes de texto publicados na túa sala activa",
|
||||
"See text messages posted to this room": "Ver mensaxes de texto publicados nesta sala",
|
||||
"Send text messages as you in your active room": "Enviar mensaxes de texto no teu nome á túa sala activa",
|
||||
"Send text messages as you in this room": "Enviar mensaxes de texto no teu nome a esta sala",
|
||||
"See messages posted to your active room": "Ver as mensaxes publicadas na túa sala activa",
|
||||
"See messages posted to this room": "Ver as mensaxes publicadas nesta sala",
|
||||
"Send messages as you in your active room": "Enviar mensaxes no teu nome na túa sala activa",
|
||||
"Send messages as you in this room": "Enviar mensaxes no teu nome nesta sala",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "Ver os eventos <b>%(eventType)s</b> publicados na túa sala activa",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "Envía no teu nome <b>%(eventType)s</b> eventos á túa sala activa",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "Ver <b>%(eventType)s</b> eventos publicados nesta sala",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "Envia no teu nome <b>%(eventType)s</b> eventos a esta sala",
|
||||
"with state key %(stateKey)s": "coa chave de estado %(stateKey)s",
|
||||
"with an empty state key": "cunha chave de estado baleiro",
|
||||
"See when anyone posts a sticker to your active room": "Ver cando alguén publica un adhesivo na túa sala activa",
|
||||
"Send stickers to your active room as you": "Enviar no teu nome adhesivos á túa sala activa",
|
||||
"See when a sticker is posted in this room": "Ver cando un adhesivo se publica nesta sala",
|
||||
"Send stickers to this room as you": "Enviar no teu nome adhesivos a esta sala",
|
||||
"See when the avatar changes in your active room": "Ver cando o avatar da túa sala activa cambie",
|
||||
"Change the avatar of your active room": "Cambiar o avatar da túa sala activa",
|
||||
"See when the avatar changes in this room": "Ver cando o avatar desta sala cambie",
|
||||
"Change the avatar of this room": "Cambiar o avatar desta sala",
|
||||
"See when the name changes in your active room": "Ver cando o nome da túa sala activa cambie",
|
||||
"Change the name of your active room": "Cambiar o tema da túa sala activa",
|
||||
"See when the name changes in this room": "Ver cando o nome desta sala cambie",
|
||||
"Change the name of this room": "Cambiar o nome desta sala",
|
||||
"See when the topic changes in your active room": "Ver cando o tema da túa sala activa cambie",
|
||||
"Change the topic of your active room": "Cambiar o tema da túa sala activa",
|
||||
"See when the topic changes in this room": "Ver cando o tema desta sala cambie",
|
||||
"Change the topic of this room": "Cambiar o tema desta sala",
|
||||
"Change which room you're viewing": "Cambiar a sala que estás vendo",
|
||||
"Send stickers into your active room": "Enviar adhesivos á túa sala activa",
|
||||
"Send stickers into this room": "Enviar adhesivos a esta sala",
|
||||
"Remain on your screen while running": "Permanecer na túa pantalla mentras se executa",
|
||||
"Remain on your screen when viewing another room, when running": "Permanecer na túa pantalla cando visualizas outra sala, ó executar",
|
||||
"Enter phone number": "Escribe número de teléfono",
|
||||
"Enter email address": "Escribe enderezo email",
|
||||
"Return to call": "Volver á chamada",
|
||||
"Fill Screen": "Encher a pantalla",
|
||||
"Voice Call": "Chamada de voz",
|
||||
"Video Call": "Chamada de vídeo",
|
||||
"New here? <a>Create an account</a>": "Acabas de coñecernos? <a>Crea unha conta</a>",
|
||||
"Got an account? <a>Sign in</a>": "Tes unha conta? <a>Conéctate</a>",
|
||||
"Render LaTeX maths in messages": "Mostrar fórmulas matemáticas LaTex",
|
||||
"No other application is using the webcam": "Outra aplicación non está usando a cámara",
|
||||
"Permission is granted to use the webcam": "Tes permiso para acceder ó uso da cámara",
|
||||
"A microphone and webcam are plugged in and set up correctly": "O micrófono e a cámara están conectados e correctamente configurados",
|
||||
"Call failed because no webcam or microphone could not be accessed. Check that:": "A chamada fallou porque non están accesibles a cámara ou o micrófono. Comproba que:",
|
||||
"Unable to access webcam / microphone": "Non se puido acceder a cámara / micrófono",
|
||||
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "A chamada fallou porque non se puido acceder a un micrófono. Comproba que o micrófono está conectado e correctamente configurado.",
|
||||
"Unable to access microphone": "Non se puido acceder ó micrófono",
|
||||
"Decide where your account is hosted": "Decide onde queres crear a túa conta",
|
||||
"Host account on": "Crea a conta en",
|
||||
"Already have an account? <a>Sign in here</a>": "Xa tes unha conta? <a>Conecta aquí</a>",
|
||||
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Ou %(usernamePassword)s",
|
||||
"Continue with %(ssoButtons)s": "Continúa con %(ssoButtons)s",
|
||||
"That username already exists, please try another.": "Ese nome de usuaria xa existe, proba con outro.",
|
||||
"New? <a>Create account</a>": "Recén cheagada? <a>Crea unha conta</a>",
|
||||
"There was a problem communicating with the homeserver, please try again later.": "Houbo un problema de comunicación co servidor de inicio, inténtao máis tarde.",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Usa o email para ser opcionalmente descubrible para os contactos existentes.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Usa un email ou teléfono para ser (opcionalmente) descubrible polos contactos existentes.",
|
||||
"Add an email to be able to reset your password.": "Engade un email para poder restablecer o contrasinal.",
|
||||
"Forgot password?": "Esqueceches o contrasinal?",
|
||||
"That phone number doesn't look quite right, please check and try again": "Non semella correcto este número, compróbao e inténtao outra vez",
|
||||
"About homeservers": "Acerca dos servidores de inicio",
|
||||
"Learn more": "Saber máis",
|
||||
"Use your preferred Matrix homeserver if you have one, or host your own.": "Usa o teu servidor de inicio Matrix preferido, ou usa o teu propio.",
|
||||
"Other homeserver": "Outro servidor de inicio",
|
||||
"We call the places you where you can host your account ‘homeservers’.": "Chamámoslle 'Servidores de inicio' ós servidores onde poderías ter a túa conta.",
|
||||
"Sign into your homeserver": "Conecta co teu servidor de inicio",
|
||||
"Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org é o servidor de inicio máis grande de todos, polo que é lugar común para moitas persoas.",
|
||||
"Specify a homeserver": "Indica un servidor de inicio",
|
||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Lembra que se non engades un email e esqueces o contrasinal <b>perderás de xeito permanente o acceso á conta</b>.",
|
||||
"Continuing without email": "Continuando sen email",
|
||||
"Continue with %(provider)s": "Continuar con %(provider)s",
|
||||
"Homeserver": "Servidor de inicio",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "Podes usar as opcións do servidor para poder conectarte a outros servidores Matrix indicando o URL dese servidor. Esto permíteche usar Element cunha conta Matrix existente noutro servidor.",
|
||||
"Server Options": "Opcións do servidor",
|
||||
"Reason (optional)": "Razón (optativa)",
|
||||
"We call the places where you can host your account ‘homeservers’.": "Ós lugares onde podes ter unha conta chamámoslle 'servidores de inicio'.",
|
||||
"Invalid URL": "URL non válido",
|
||||
"Unable to validate homeserver": "Non se puido validar o servidor de inicio",
|
||||
"sends confetti": "envía confetti",
|
||||
"Sends the given message with confetti": "Envía a mensaxe con confetti",
|
||||
"Show chat effects": "Mostrar efectos do chat",
|
||||
"Effects": "Efectos",
|
||||
"Call failed because webcam or microphone could not be accessed. Check that:": "A chamada fallou porque non tiñas acceso á cámara ou ó micrófono. Comproba:",
|
||||
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "A chamada fallou porque non tiñas acceso ó micrófono. Comproba que o micrófono está conectado e correctamente configurado.",
|
||||
"Hold": "Colgar",
|
||||
"Resume": "Retomar",
|
||||
"%(peerName)s held the call": "%(peerName)s finalizou a chamada",
|
||||
"You held the call <a>Resume</a>": "Colgaches a chamada, <a>Retomar</a>",
|
||||
"You've reached the maximum number of simultaneous calls.": "Acadaches o número máximo de chamadas simultáneas.",
|
||||
"Too Many Calls": "Demasiadas chamadas",
|
||||
"%(name)s paused": "detido por %(name)s"
|
||||
}
|
||||
|
|
|
@ -2663,8 +2663,8 @@
|
|||
"Bolivia": "Bolívia",
|
||||
"Bhutan": "Bhután",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Téma: %(topic)s (<a>szerkesztés</a>)",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "Ez a közvetlen üzeneteinek előzményeinek eleje a következővel: <displayName/>.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Csak ketten vannak ebben a beszélgetésben, hacsak valamelyikőjük nem hív meg valakit, hogy csatlakozzon.",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "Ez a közvetlen beszélgetés kezdete <displayName/> felhasználóval.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Csak önök ketten vannak ebben a beszélgetésben, hacsak valamelyikőjük nem hív meg valakit, hogy csatlakozzon.",
|
||||
"Call Paused": "Hívás szüneteltetve",
|
||||
"Takes the call in the current room off hold": "Visszaveszi tartásból a jelenlegi szoba hívását",
|
||||
"Places the call in the current room on hold": "Tartásba teszi a jelenlegi szoba hívását",
|
||||
|
@ -2832,11 +2832,19 @@
|
|||
"Gibraltar": "Gibraltár",
|
||||
"%(creator)s created this DM.": "%(creator)s hozta létre ezt az üzenetet.",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "A szobában lévő üzenetek végpontok között titkosítottak. Miután csatlakoztak a felhasználók, ellenőrizheted őket a profiljukban, amit a profilképükre kattintással nyithatsz meg.",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Az üzenetek végpontok között titkosítottak. Ellenőrizze %(displayName)s személyazonosságát a profilján – koppintson a profilképére.",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Az üzenetek végpontok között titkosítottak. Ellenőrizze %(displayName)s személyazonosságát a profilján – kattintson %(displayName)s profilképére.",
|
||||
"This is the start of <roomName/>.": "Ez a(z) <roomName/> kezdete.",
|
||||
"Add a photo, so people can easily spot your room.": "Állíts be egy fényképet, hogy az emberek könnyebben felismerjék a szobát!",
|
||||
"%(displayName)s created this room.": "%(displayName)s készítette ezt a szobát.",
|
||||
"You created this room.": "Te készítetted ezt a szobát.",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Állítsd be a szoba témáját</a>, hogy az emberek tudják, hogy miről van itt szó.",
|
||||
"Topic: %(topic)s ": "Téma: %(topic)s "
|
||||
"Topic: %(topic)s ": "Téma: %(topic)s ",
|
||||
"Send stickers to this room as you": "Ön helyett matricák küldése a szobába",
|
||||
"Change the avatar of this room": "A szoba képének megváltoztatása",
|
||||
"Change the name of this room": "A szoba nevének megváltoztatása",
|
||||
"Change the topic of your active room": "Az aktív szoba témájának megváltoztatása",
|
||||
"Change the topic of this room": "A szoba témájának megváltoztatása",
|
||||
"Change which room you're viewing": "Az ön által nézett szoba megváltoztatása",
|
||||
"Send stickers into your active room": "Matricák küldése az ön aktív szobájába",
|
||||
"Send stickers into this room": "Matricák küldése a szobába"
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue