Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into new/room-alias-in-permalink

This commit is contained in:
Simon Merrick 2020-12-07 10:30:50 +13:00
commit b365d3b27e
98 changed files with 3150 additions and 1812 deletions

View file

@ -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/

View file

@ -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/

View file

@ -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",

View file

@ -45,8 +45,6 @@
@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";
@ -78,11 +76,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";
@ -124,6 +124,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";

View file

@ -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;
@ -91,6 +85,8 @@ limitations under the License.
}
div.mx_AccessibleButton_kind_link.mx_Login_forgot {
display: block;
margin: 0 auto;
// style it as a link
font-size: inherit;
padding: 0;

View file

@ -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 {

View file

@ -1,35 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
.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);
}
}

View file

@ -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;
}

View file

@ -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;
}
export function mostRecentActivityFirst(roomList) {
return roomList.sort(function(a, b) {
return tsOfNewestEvent(b) - tsOfNewestEvent(a);
});
.mx_Dialog_primary {
width: 100%;
}
}

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

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

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

View file

@ -17,7 +17,7 @@ limitations under the License.
.mx_CallView {
border-radius: 10px;
background-color: $input-lighter-bg-color;
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

View 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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

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

12
scripts/ci/app-tests.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
#
# script which is run by the CI build (after `yarn test`).
#
# clones element-web develop and runs the tests against our version of react-sdk.
set -ev
scripts/ci/layered.sh
cd element-web
yarn build:genfiles # so the tests can run. Faster version of `build`
yarn test

View file

@ -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"

View file

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

View file

@ -1,12 +0,0 @@
#!/bin/bash
#
# 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.
set -ev
scripts/ci/layered-riot-web.sh
cd ../riot-web
yarn build:genfiles # so the tests can run. Faster version of `build`
yarn test

View file

@ -0,0 +1 @@
app-tests.sh

View file

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

View file

@ -248,15 +248,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 {

View file

@ -306,8 +306,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."),

View file

@ -29,10 +29,25 @@ 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";
// eslint-disable-next-line camelcase
identity_providers: IIdentityProvider[];
"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 +63,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 +77,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 +113,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,

View file

@ -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({

View file

@ -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()}
/>
);

View file

@ -1332,7 +1332,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 +1366,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,
});

View file

@ -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;

View file

@ -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"
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")}
/>
</div>
);
};
@ -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>

View file

@ -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,60 +146,7 @@ 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({
@ -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,13 +225,7 @@ 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
});
const flows = await loginLogic.getFlows();
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
if (hasSsoFlow) {
if (ssoFlow) {
// Redirect to login page - server probably expects SSO only
dis.dispatch({action: 'start_login'});
} else {
@ -299,10 +236,6 @@ export default class Registration extends React.Component<IProps, IState> {
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);
showGenericError(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,
@ -453,21 +388,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 +436,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,7 +460,37 @@ export default class Registration extends React.Component<IProps, IState> {
<Spinner />
</div>;
} else if (this.state.flows.length) {
return <RegistrationForm
let ssoSection;
if (this.state.ssoFlow) {
let continueWithSection;
const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"]
|| this.state.ssoFlow["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}
@ -620,8 +500,8 @@ export default class Registration extends React.Component<IProps, IState> {
flows={this.state.flows}
serverConfig={this.props.serverConfig}
canSubmit={!this.state.serverErrorIsFatal}
serverRequiresIdServer={this.state.serverRequiresIdServer}
/>;
/>
</React.Fragment>;
}
}
@ -650,13 +530,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 +584,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 }

View file

@ -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>
);

View file

@ -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>
);
}
}

View file

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

View file

@ -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>
);
}
}

View file

@ -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
forgotPasswordJsx = <AccessibleButton
className="mx_Login_forgot"
disabled={this.props.busy}
kind="link"
onClick={this.onForgotPasswordClick}
>
{sub}
</AccessibleButton>
),
})}
</span>;
{_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}

View file

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

View file

@ -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>
);
}
}

View file

@ -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>;
}
}

View file

@ -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>;
}
}

View file

@ -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;

View file

@ -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();
};

View file

@ -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>
);
}
}

View file

@ -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 }

View file

@ -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;

View file

@ -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();
}
};

View file

@ -0,0 +1,203 @@
/*
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 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;
this.state = {
defaultChosen: this.props.serverConfig.isDefault,
otherHomeserver: this.props.serverConfig.isDefault ? "" : this.props.serverConfig.hsUrl,
};
}
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: hsUrl }) => {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl) return {};
try {
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl);
return {};
} catch (e) {
console.error(e);
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
if (!stateForError.isFatalError) {
// carry on anyway
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true);
return {};
} else {
let error = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
error = e.translatedMessage;
}
return { error };
}
}
},
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 its 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 you 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>;
}
}

View file

@ -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,
});
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) => {
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,
});
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;

View file

@ -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;

View 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.identity_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;

View 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.hsName;
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;

View file

@ -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');
}

View file

@ -83,9 +83,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 +101,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 +119,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 +184,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") }

View file

@ -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();

View file

@ -745,9 +745,18 @@ 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}
<MemberAvatar member={member}
width={avatarSize} height={avatarSize}
viewUserOnClick={true}
/>

View file

@ -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;

View file

@ -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": "تعذر الوصول إلى الميكروفون"
}

View file

@ -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",
@ -2552,5 +2552,366 @@
"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 dIvoire": "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"
}

View file

@ -1136,6 +1136,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.",
@ -1895,6 +1897,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",
@ -1955,6 +1962,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",
@ -1989,8 +1997,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",
@ -2113,6 +2119,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 didnt sign in to this session, your account may be compromised.": "If you didnt 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.",
@ -2146,6 +2156,15 @@
"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/identity server": "Unable to validate homeserver/identity server",
"Specify a homeserver": "Specify a homeserver",
"Matrix.org is the biggest public homeserver in the world, so its a good place for many.": "Matrix.org is the biggest public homeserver in the world, so its a good place for many.",
"Sign into your homeserver": "Sign into your homeserver",
"We call the places you where you can host your account homeservers.": "We call the places you 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",
@ -2274,8 +2293,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.",
@ -2289,48 +2306,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",
@ -2486,13 +2485,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.",
@ -2514,24 +2510,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.",

View file

@ -2846,5 +2846,87 @@
"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"
}

View file

@ -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": "Eluotettu",
"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.",
@ -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,118 @@
"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 youre 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"
}

View file

@ -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",

View file

@ -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,87 @@
"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"
}

View file

@ -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"
}

View file

@ -2848,5 +2848,9 @@
"Filter rooms and people": "Filtra stanze e persone",
"Open the link in the email to continue registration.": "Apri il link nell'email per continuare la registrazione.",
"A confirmation email has been sent to %(emailAddress)s": "È stata inviata un'email di conferma a %(emailAddress)s",
"Start a new chat": "Inizia una nuova chat"
"Start a new chat": "Inizia una nuova chat",
"Go to Home View": "Vai alla vista home",
"<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 per la pagina della tua comunità</h1>\n<p>\n Usa la descrizione estesa per introdurre i nuovi membri alla comunità, o distribuisci\n alcuni <a href=\"foo\">link</a> importanti\n</p>\n<p>\n Puoi anche aggiungere immagini con gli URL Matrix <img src=\"mxc://url\" />\n</p>\n",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Salva in cache i messaggi cifrati localmente in modo che appaiano nei risultati di ricerca, usando %(size)s per salvarli da %(rooms)s stanza.",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Salva in cache i messaggi cifrati localmente in modo che appaiano nei risultati di ricerca, usando %(size)s per salvarli da %(rooms)s stanze."
}

View file

@ -218,7 +218,7 @@
"Room %(roomId)s not visible": "Kambarys %(roomId)s nematomas",
"/ddg is not a command": "/ddg nėra komanda",
"Changes your display nickname": "Pakeičia jūsų rodomą slapyvardį",
"Invites user with given id to current room": "Pakviečia naudotoją su nurodytu id į esamą kambarį",
"Invites user with given id to current room": "Pakviečia vartotoją su nurodytu id į dabartinį kambarį",
"You are now ignoring %(userId)s": "Dabar ignoruojate %(userId)s",
"Opens the Developer Tools dialog": "Atveria Programuotojo Įrankių dialogą",
"Verified key": "Patvirtintas raktas",
@ -272,7 +272,7 @@
"%(senderName)s uploaded a file": "%(senderName)s įkėlė failą",
"Options": "Parinktys",
"Key request sent.": "Rakto užklausa išsiųsta.",
"Failed to mute user": "Nepavyko nutildyti naudotoją",
"Failed to mute user": "Nepavyko nutildyti vartotojo",
"Are you sure?": "Ar tikrai?",
"Ignore": "Ignoruoti",
"Invite": "Pakviesti",
@ -422,13 +422,13 @@
"Autoplay GIFs and videos": "Automatiškai paleisti GIF ir vaizdo įrašus",
"This event could not be displayed": "Nepavyko parodyti šio įvykio",
"Kick": "Išmesti",
"Kick this user?": "Išmesti šį naudotoją?",
"Kick this user?": "Išmesti šį vartotoją?",
"Failed to kick": "Nepavyko išmesti",
"Unban": "Atblokuoti",
"Ban": "Užblokuoti",
"Unban this user?": "Atblokuoti šį naudotoją?",
"Ban this user?": "Užblokuoti šį naudotoją?",
"Failed to ban user": "Nepavyko užblokuoti naudotoją",
"Unban this user?": "Atblokuoti šį vartotoją?",
"Ban this user?": "Užblokuoti šį vartotoją?",
"Failed to ban user": "Nepavyko užblokuoti vartotojo",
"Invited": "Pakviestas",
"Filter room members": "Filtruoti kambario dalyvius",
"Server unavailable, overloaded, or something else went wrong.": "Serveris neprieinamas, perkrautas arba nutiko kažkas kito.",
@ -661,7 +661,7 @@
"Public Chat": "Viešas pokalbis",
"There are no visible files in this room": "Šiame kambaryje nėra matomų failų",
"Add a Room": "Pridėti kambarį",
"Add a User": "Pridėti naudotoją",
"Add a User": "Pridėti Vartotoją",
"Long Description (HTML)": "Ilgasis aprašas (HTML)",
"Description": "Aprašas",
"Community %(groupId)s not found": "Bendruomenė %(groupId)s nerasta",
@ -2010,5 +2010,22 @@
"Verify this session by confirming the following number appears on its screen.": "Patvirtinkite šį seansą, įsitikindami, kad jo ekrane rodomas toliau esantis skaičius.",
"Privacy": "Privatumas",
"Accept all %(invitedRooms)s invites": "Priimti visus %(invitedRooms)s pakvietimus",
"Bulk options": "Grupinės parinktys"
"Bulk options": "Grupinės parinktys",
"Confirm Security Phrase": "Patvirtinkite Slaptafrazę",
"Set a Security Phrase": "Nustatyti Slaptafrazę",
"Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Naudokite slaptafrazę, kurią žinote tik jūs ir pasirinktinai išsaugokite Apsaugos Raktą, naudoti kaip atsarginę kopiją.",
"Enter a Security Phrase": "Įveskite Slaptafrazę",
"Security Phrase": "Slaptafrazė",
"Enter a security phrase only you know, as its used to safeguard your data. To be secure, you shouldnt re-use your account password.": "Įveskite slaptafrazę, kurią žinote tik jūs, nes ji naudojama jūsų duomenims apsaugoti. Tam, kad būtumėte saugūs, neturėtumėte vėl naudoti savo paskyros slaptažodžio.",
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Įveskite savo Slaptafrazę arba <button>Naudokite savo Apsaugos Raktą</button>, kad tęstumėte.",
"%(creator)s created this DM.": "%(creator)s sukūrė šį tiesioginio susirašymo kambarį.",
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Šiame pokalbyje esate tik jūs dviese, nebent kuris nors iš jūsų pakvies ką nors prisijungti.",
"This is the beginning of your direct message history with <displayName/>.": "Tai yra jūsų tiesioginių žinučių su <displayName/> istorijos pradžia.",
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Žinutės čia yra visapusiškai užšifruotos. Patvirtinkite %(displayName)s jų profilyje - paspauskite ant jų pseudoportreto.",
"See when the avatar changes in your active room": "Matyti kada jūsų aktyviame kambaryje pasikeičia pseudoportretas",
"Change the avatar of your active room": "Pakeisti jūsų aktyvaus kambario pseudoportretą",
"See when the avatar changes in this room": "Matyti kada šiame kambaryje pasikeičia pseudoportretas",
"Change the avatar of this room": "Pakeisti šio kambario pseudoportretą",
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Žinutės šiame kambaryje yra visapusiškai užšifruotos. Kai žmonės prisijungia, jūs galite patvirtinti juos jų profilyje, tiesiog paspauskite ant jų pseudoportreto.",
"Mentions & Keywords": "Paminėjimai ir Raktažodžiai"
}

View file

@ -385,7 +385,7 @@
"Error decrypting audio": "Błąd deszyfrowania audio",
"Error decrypting image": "Błąd deszyfrowania obrazu",
"Error decrypting video": "Błąd deszyfrowania wideo",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Próbowano załadować konkretny punkt na osi czasu w tym pokoju, ale nie nie można go znaleźć.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Próbowano załadować konkretny punkt na osi czasu w tym pokoju, ale nie można go znaleźć.",
"The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Wyeksportowany plik pozwoli każdej osobie będącej w stanie go odczytać na deszyfrację jakichkolwiek zaszyfrowanych wiadomości, które możesz zobaczyć, tak więc zalecane jest zachowanie ostrożności. Aby w tym pomóc, powinieneś/aś wpisać hasło poniżej; hasło to będzie użyte do zaszyfrowania wyeksportowanych danych. Późniejsze zaimportowanie tych danych będzie możliwe tylko po uprzednim podaniu owego hasła.",
" (unsupported)": " (niewspierany)",
"Idle": "Bezczynny(-a)",
@ -1584,5 +1584,391 @@
"Cancel entering passphrase?": "Anulować wpisywanie hasła?",
"Room name or address": "Nazwa lub adres pokoju",
"This will end the conference for everyone. Continue?": "Czy na pewno chcesz zakończyc połączenie grupowe? To zakończy je dla wszystkich uczestnikow.",
"End conference": "Zakończ połączenie grupowe"
"End conference": "Zakończ połączenie grupowe",
"Attach files from chat or just drag and drop them anywhere in a room.": "Załącz pliki w rozmowie lub upuść je w dowolnym miejscu rozmowy.",
"Sign in with SSO": "Zaloguj się z SSO",
"No files visible in this room": "Brak plików widocznych w tym pokoju",
"Document": "Dokument",
"Service": "Usługa",
"Summary": "Opis",
"To continue you need to accept the terms of this service.": "Aby kontynuować, musisz zaakceptować zasady użytkowania.",
"Connecting to integration manager...": "Łączenie z zarządcą integracji…",
"Add widgets, bridges & bots": "Dodaj widżety, mostki i boty",
"Forget this room": "Zapomnij o tym pokoju",
"You were kicked from %(roomName)s by %(memberName)s": "Zostałeś(-aś) wyrzucony(-a) z %(roomName)s przez %(memberName)s",
"List options": "Ustawienia listy",
"Explore all public rooms": "Przeglądaj wszystkie publiczne pokoje",
"Explore public rooms": "Przeglądaj publiczne pokoje",
"Verification Requests": "Żądania weryfikacji",
"View Servers in Room": "Zobacz serwery w pokoju",
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Zmiany tego, kto może przeglądać historię wyszukiwania dotyczą tylko przyszłych wiadomości w pokoju. Widoczność wcześniejszej historii nie zmieni się.",
"No other published addresses yet, add one below": "Brak innych opublikowanych adresów, dodaj jakiś poniżej",
"Other published addresses:": "Inne opublikowane adresy:",
"Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Opublikowane adresy mogą być używane, aby każdy mógł dołączyć do Twojego pokoju. Aby opublikować adres, należy wcześniej ustawić lokalny adres.",
"Room settings": "Ustawienia pokoju",
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Wiadomości w tym pokoju są szyfrowane end-to-end. Jeżeli ludzie dołączą do niego, możesz zweryfikować ich na ich profilu, naciskając na ich awatar.",
"Messages in this room are not end-to-end encrypted.": "Wiadomości w tym pokoju nie są szyfrowane end-to-end.",
"Show files": "Zobacz pliki",
"%(count)s people|one": "%(count)s osoba",
"%(count)s people|other": "%(count)s ludzi(e)",
"About": "Informacje",
"<a>Add a topic</a> to help people know what it is about.": "<a>Dodaj temat</a>, aby poinformować ludzi czego to dotyczy.",
"Show info about bridges in room settings": "Pokazuj informacje o mostkach w ustawieniach pokoju",
"about a day from now": "około dnia od teraz",
"about an hour from now": "około godziny od teraz",
"about a minute from now": "około minuty od teraz",
"Room Info": "Informacje o pokoju",
"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.": "Zgłoszenie tej wiadomości wyśle administratorowi serwera unikatowe „ID wydarzenia”. Jeżeli wiadomości w tym pokoju są szyfrowane, administrator serwera może nie być w stanie przeczytać treści wiadomości, lub zobaczyć plików bądź zdjęć.",
"Send report": "Wyślij zgłoszenie",
"Report Content to Your Homeserver Administrator": "Zgłoś zawartość do administratora swojego serwera",
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Prywatne pokoje można odnaleźć i dołączyć do nich tylko przez zaproszenie. Do publicznych pokojów może dołączyć każdy w tej społeczności.",
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Prywatne pokoje można odnaleźć i dołączyć do nich tylko przez zaproszenie. Do publicznych pokojów każdy może dołączyć.",
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Możesz ustawić tę opcję, jeżeli pokój będzie używany wyłącznie do współpracy wewnętrznych zespołów na Twoim serwerze. To nie może być później zmienione.",
"Block anyone not part of %(serverName)s from ever joining this room.": "Zablokuj wszystkich niebędących użytkownikami %(serverName)s w tym pokoju.",
"You cant disable this later. Bridges & most bots wont work yet.": "Nie możesz wyłączyć tego później. Mostki i większość botów nie będą działać.",
"Matrix rooms": "Pokoje Matrix",
"Start a conversation with someone using their name or username (like <userId/>).": "Rozpocznij konwersację z innymi korzystając z ich nazwy lub nazwy użytkownika (np. <userId/>).",
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Rozpocznij konwersację z innymi korzystając z ich nazwy, adresu e-mail lub nazwy użytkownika (np. <userId/>).",
"Show %(count)s more|one": "Pokaż %(count)s więcej",
"Show %(count)s more|other": "Pokaż %(count)s więcej",
"Room options": "Ustawienia pokoju",
"Manually verify all remote sessions": "Ręcznie weryfikuj wszystkie zdalne sesje",
"Privacy": "Prywatność",
"This version of %(brand)s does not support searching encrypted messages": "Ta wersja %(brand)s nie obsługuje wyszukiwania zabezpieczonych wiadomości",
"Use the <a>Desktop app</a> to search encrypted messages": "Używaj <a>Aplikacji desktopowej</a>, aby wyszukiwać zaszyfrowane wiadomości",
"Message search": "Wyszukiwanie wiadomości",
"Enable message search in encrypted rooms": "Włącz wyszukiwanie wiadomości w szyfrowanych pokojach",
"New version of %(brand)s is available": "Dostępna jest nowa wersja %(brand)s",
"Update %(brand)s": "Aktualizuj %(brand)s",
"Set up Secure Backup": "Skonfiguruj bezpieczny backup",
"Ok": "OK",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Wysyłaj <UsageDataLink>anonimowe dane o wykorzystywaniu</UsageDataLink>, które pomogą nam usprawnić %(brand)s. To będzie korzystać z <PolicyLink>pliku cookie</PolicyLink>.",
"Help us improve %(brand)s": "Pomóż nam usprawnić %(brand)s",
"Unknown App": "Nieznana aplikacja",
"Enable desktop notifications": "Włącz powiadomienia na pulpicie",
"Don't miss a reply": "Nie przegap odpowiedzi",
"A session's public name is visible to people you communicate with": "Publiczna nazwa sesji jest widoczna dla osób z którymi się komunikujesz",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Zarządzaj nazwami i unieważnaj sesje poniżej, lub <a>weryfikuj je na swoim profilu</a>.",
"Where youre logged in": "Gdzie jesteś zalogowany(-a)",
"Review where youre logged in": "Przejrzyj, gdzie jesteś zalogowany(-a)",
"Show tray icon and minimize window to it on close": "Pokazuj ikonę w zasobniku i minimalizuj okno do zasobnika przy zamknięciu",
"Display your community flair in rooms configured to show it.": "Wyświetlaj swój wyróżnik społeczności w pokojach skonfigurowanych, aby go używać.",
"System font name": "Nazwa czcionki systemowej",
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Wybierz nazwę czcionki zainstalowanej w systemie, a %(brand)s spróbuje jej użyć.",
"Use a system font": "Użyj czcionki systemowej",
"Enable experimental, compact IRC style layout": "Włącz eksperymentalny, kompaktowy układ w stylu IRC",
"Use a more compact Modern layout": "Użyj bardziej kompaktowego „nowoczesnego” układu",
"Use custom size": "Użyj niestandardowego rozmiaru",
"Appearance Settings only affect this %(brand)s session.": "Ustawienia wyglądu wpływają tylko na tę sesję %(brand)s.",
"Customise your appearance": "Dostosuj wygląd",
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji aby zarządzać botami, widżetami i pakietami naklejek.",
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji <b>%(serverName)s</b> aby zarządzać botami, widżetami i pakietami naklejek.",
"There are two ways you can provide feedback and help us improve %(brand)s.": "Są dwa sposoby na przekazanie informacji zwrotnych i pomoc w usprawnieniu %(brand)s.",
"Feedback sent": "Wysłano informacje zwrotne",
"Send feedback": "Wyślij informacje zwrotne",
"Feedback": "Informacje zwrotne",
"You have no visible notifications in this room.": "Nie masz widocznych powiadomień w tym pokoju.",
"%(creator)s created this DM.": "%(creator)s utworzył(a) tę wiadomość bezpośrednią.",
"You do not have permission to create rooms in this community.": "Nie masz uprawnień do tworzenia pokojów w tej społeczności.",
"Cannot create rooms in this community": "Nie można utworzyć pokojów w tej społeczności",
"Liberate your communication": "Uwolnij swoją komunikację",
"Welcome to %(appName)s": "Witamy w %(appName)s",
"Now, let's help you get started": "Teraz pomożemy Ci zacząć",
"Welcome %(name)s": "Witaj, %(name)s",
"Israel": "Izrael",
"Isle of Man": "Man",
"Ireland": "Irlandia",
"Iraq": "Irak",
"Iran": "Iran",
"Indonesia": "Indonezja",
"India": "Indie",
"Iceland": "Islandia",
"Hungary": "Węgry",
"Hong Kong": "Hong Kong",
"Honduras": "Honduras",
"Heard & McDonald Islands": "Wyspy Heard i McDonald",
"Haiti": "Haiti",
"Guyana": "Gujana",
"Guinea-Bissau": "Gwinea Bissau",
"Guinea": "Gwinea",
"Guernsey": "Guernsey",
"Guatemala": "Gwatemala",
"Guam": "Guam",
"Guadeloupe": "Gwadelupa",
"Grenada": "Grenada",
"Greenland": "Grenlandia",
"Greece": "Grecja",
"Gibraltar": "Gibraltar",
"Ghana": "Ghana",
"Germany": "Niemcy",
"Georgia": "Gruzja",
"Gambia": "Gambia",
"Gabon": "Gabon",
"French Southern Territories": "Francuskie Terytoria Południowe i Antarktyczne",
"French Polynesia": "Polinezja Francuska",
"French Guiana": "Gujana Francuska",
"France": "Francja",
"Finland": "Finlandia",
"Fiji": "Fidżi",
"Faroe Islands": "Wyspy Owcze",
"Falkland Islands": "Falklandy",
"Ethiopia": "Etiopia",
"Estonia": "Estonia",
"Eritrea": "Erytrea",
"Equatorial Guinea": "Gwinea Równikowa",
"El Salvador": "Salwador",
"Egypt": "Egipt",
"Ecuador": "Ekwador",
"Dominican Republic": "Dominikana",
"Dominica": "Dominika",
"Djibouti": "Dżibuti",
"Denmark": "Dania",
"Côte dIvoire": "Wybrzeże Kości Słoniowej",
"Czech Republic": "Czechy",
"Cyprus": "Cypr",
"Curaçao": "Curaçao",
"Cuba": "Kuba",
"Croatia": "Chorwacja",
"Costa Rica": "Kostaryka",
"Cook Islands": "Wyspy Cooka",
"Congo - Kinshasa": "Kinszasa",
"Congo - Brazzaville": "Kongo",
"Comoros": "Komory",
"Colombia": "Kolumbia",
"Cocos (Keeling) Islands": "Wyspy Kokosowe",
"Christmas Island": "Wyspa Bożego Narodzenia",
"China": "Chiny",
"Chile": "Chile",
"Chad": "Czad",
"Central African Republic": "Republika Środkowoafrykańska",
"Cayman Islands": "Kajmany",
"Caribbean Netherlands": "Holandia Karaibska",
"Cape Verde": "Republika Zielonego Przylądka",
"Canada": "Kanada",
"Cameroon": "Kamerun",
"Cambodia": "Kambodża",
"Burundi": "Burundi",
"Burkina Faso": "Burkina Faso",
"Bulgaria": "Bułgaria",
"Brunei": "Brunei",
"British Virgin Islands": "Brytyjskie Wyspy Dziewicze",
"British Indian Ocean Territory": "Brytyjskie Terytorium Oceanu Indyjskiego",
"Brazil": "Brazylia",
"Bouvet Island": "Wyspa Bouveta",
"Botswana": "Botswana",
"Bosnia": "Bośnia",
"Bolivia": "Boliwia",
"Bhutan": "Bhutan",
"Bermuda": "Bermudy",
"Benin": "Benin",
"Belize": "Belize",
"Belgium": "Belgia",
"Belarus": "Białoruś",
"Barbados": "Barbados",
"Bangladesh": "Bangladesz",
"Bahrain": "Bahrajn",
"Bahamas": "Bahamy",
"Azerbaijan": "Azerbejdżan",
"Austria": "Austria",
"Australia": "Australia",
"Aruba": "Aruba",
"Armenia": "Armenia",
"Argentina": "Argentyna",
"Antigua & Barbuda": "Antigua i Barbuda",
"Antarctica": "Antarktyda",
"Anguilla": "Anguilla",
"Angola": "Angola",
"Andorra": "Andora",
"American Samoa": "Samoa Amerykańskie",
"Algeria": "Algeria",
"Albania": "Albania",
"Åland Islands": "Wyspy Alandzkie",
"Afghanistan": "Afganistan",
"United States": "Stany Zjednoczone",
"United Kingdom": "Wielka Brytania",
"Marshall Islands": "Wyspy Marshalla",
"Malta": "Malta",
"Mali": "Mali",
"Maldives": "Malediwy",
"Malaysia": "Malezja",
"Malawi": "Malawi",
"Madagascar": "Madagaskar",
"Macedonia": "Macedonia",
"Macau": "Makau",
"Luxembourg": "Luksemburg",
"Lithuania": "Litwa",
"Liechtenstein": "Liechtenstein",
"Libya": "Libia",
"Liberia": "Liberia",
"Lesotho": "Lesotho",
"Lebanon": "Liban",
"Latvia": "Łotwa",
"Laos": "Laos",
"Kyrgyzstan": "Kirgistan",
"Kuwait": "Kuwejt",
"Kosovo": "Kosowo",
"Kiribati": "Kiribati",
"Kenya": "Kenia",
"Kazakhstan": "Kazachstan",
"Jordan": "Jordania",
"Jersey": "Jersey",
"User rules": "Zasady użytkownika",
"Server rules": "Zasady serwera",
"not found": "nie znaleziono",
"Decline (%(counter)s)": "Odrzuć (%(counter)s)",
"Starting backup...": "Rozpoczynanie kopii zapasowej…",
"User Autocomplete": "Autouzupełnianie użytkowników",
"Community Autocomplete": "Autouzupełnianie społeczności",
"Room Autocomplete": "Autouzupełnianie pokojów",
"Notification Autocomplete": "Autouzupełnianie powiadomień",
"Emoji Autocomplete": "Autouzupełnianie emoji",
"Phone (optional)": "Telefon (opcjonalny)",
"Upload Error": "Błąd wysyłania",
"GitHub issue": "Błąd na GitHubie",
"Close dialog": "Zamknij okno dialogowe",
"Show all": "Zobacz wszystko",
"Deactivate user": "Dezaktywuj użytkownika",
"Deactivate user?": "Dezaktywować użytkownika?",
"Revoke invite": "Wygaś zaproszenie",
"Code block": "Blok kodu",
"Ban users": "Zablokuj użytkowników",
"Kick users": "Wyrzuć użytkowników",
"Syncing...": "Synchronizacja…",
"General failure": "Ogólny błąd",
"Removing…": "Usuwanie…",
"Premium": "Premium",
"Cancelling…": "Anulowanie…",
"Algorithm:": "Algorytm:",
"Bulk options": "Masowe działania",
"Modern": "Współczesny",
"Compact": "Kompaktowy",
"Approve": "Zatwierdź",
"Incompatible Database": "Niekompatybilna baza danych",
"Show": "Pokaż",
"Information": "Informacje",
"Categories": "Kategorie",
"Reactions": "Reakcje",
"Role": "Rola",
"Trusted": "Zaufane",
"Accepting…": "Akceptowanie…",
"Re-join": "Dołącz ponownie",
"Unencrypted": "Nieszyfrowane",
"Revoke": "Unieważnij",
"Encrypted": "Szyfrowane",
"Unsubscribe": "Odsubskrybuj",
"None": "Brak",
"exists": "istnieje",
"Change the topic of this room": "Zmień temat tego pokoju",
"Change which room you're viewing": "Zmień pokój który przeglądasz",
"Send stickers into your active room": "Wyślij naklejki w swoim aktywnym pokoju",
"Send stickers into this room": "Wyślij naklejki w tym pokoju",
"Zimbabwe": "Zimbabwe",
"Zambia": "Zambia",
"Yemen": "Jemen",
"Western Sahara": "Sahara Zachodnia",
"Wallis & Futuna": "Wallis i Futuna",
"Vietnam": "Wietnam",
"Venezuela": "Wenezuela",
"Vatican City": "Watykan",
"Vanuatu": "Vanuatu",
"Uzbekistan": "Uzbekistan",
"Uruguay": "Urugwaj",
"United Arab Emirates": "Zjednoczone Emiraty Arabskie",
"Ukraine": "Ukraina",
"Uganda": "Uganda",
"U.S. Virgin Islands": "Wyspy Dziewicze Stanów Zjednoczonych",
"Tuvalu": "Tuvalu",
"Turks & Caicos Islands": "Turks i Caicos",
"Turkmenistan": "Turkmenistan",
"Turkey": "Turcja",
"Tunisia": "Tunezja",
"Trinidad & Tobago": "Trynidad i Tobago",
"Tonga": "Tonga",
"Tokelau": "Tokelau",
"Togo": "Togo",
"Timor-Leste": "Timor Wschodni",
"Thailand": "Tajlandia",
"Tanzania": "Tanzania",
"Tajikistan": "Tadżykistan",
"Taiwan": "Tajwan",
"São Tomé & Príncipe": "Wyspy Świętego Tomasza i Książęca",
"Syria": "Syria",
"Switzerland": "Szwajcaria",
"Sweden": "Szwecja",
"Swaziland": "Eswatini",
"Svalbard & Jan Mayen": "Svalbard i Jan Mayen",
"Suriname": "Surinam",
"Sudan": "Sudan",
"St. Vincent & Grenadines": "Saint Vincent i Grenadyny",
"St. Pierre & Miquelon": "Saint-Pierre i Miquelon",
"St. Martin": "Sint Maarten",
"St. Lucia": "Saint Lucia",
"St. Kitts & Nevis": "Saint Kitts & Nevis",
"St. Helena": "Święta Helena",
"St. Barthélemy": "Wspólnota Saint-Barthélemy",
"Sri Lanka": "Sri Lanka",
"Spain": "Hiszpania",
"South Sudan": "Sudan Południowy",
"South Korea": "Korea Południowa",
"South Georgia & South Sandwich Islands": "Georgia Południowa i Sandwich Południowy",
"South Africa": "Republika Południowej Afryki",
"Somalia": "Somalia",
"Solomon Islands": "Wyspy Salomona",
"Slovenia": "Słowenia",
"Slovakia": "Słowacja",
"Sint Maarten": "Sint Maarten",
"Singapore": "Singapur",
"Sierra Leone": "Sierra Leone",
"Seychelles": "Seszele",
"Serbia": "Serbia",
"Senegal": "Senegal",
"Saudi Arabia": "Arabia Saudyjska",
"San Marino": "San Marino",
"Samoa": "Samoa",
"Réunion": "Reunion",
"Rwanda": "Rwanda",
"Russia": "Rosja",
"Romania": "Rumunia",
"Qatar": "Katar",
"Puerto Rico": "Portoryko",
"Portugal": "Portugalia",
"Poland": "Polska",
"Pitcairn Islands": "Pitcairn",
"Philippines": "Filipiny",
"Peru": "Peru",
"Paraguay": "Paragwaj",
"Papua New Guinea": "Papua Nowa Gwinea",
"Panama": "Panama",
"Palestine": "Palestyna",
"Palau": "Palau",
"Pakistan": "Pakistan",
"Oman": "Oman",
"Norway": "Norwegia",
"Northern Mariana Islands": "Mariany Północne",
"North Korea": "Korea Północna",
"Norfolk Island": "Norfolk",
"Niue": "Niue",
"Nigeria": "Nigeria",
"Niger": "Niger",
"Nicaragua": "Nikaragua",
"New Zealand": "Nowa Zelandia",
"New Caledonia": "Nowa Kaledonia",
"Netherlands": "Holandia",
"Nepal": "Nepal",
"Nauru": "Nauru",
"Namibia": "Namibia",
"Myanmar": "Mjanma",
"Mozambique": "Mozambik",
"Morocco": "Maroko",
"Montserrat": "Montserrat",
"Montenegro": "Czarnogóra",
"Mongolia": "Mongolia",
"Monaco": "Monako",
"Moldova": "Mołdawia",
"Micronesia": "Mikronezja",
"Mexico": "Meksyk",
"Mayotte": "Majotta",
"Mauritius": "Mauritius",
"Mauritania": "Mauretania",
"Martinique": "Martynika"
}

View file

@ -501,7 +501,7 @@
"Visibility in Room List": "Visibilidade na lista de salas",
"Visible to everyone": "Visível para todos",
"Only visible to community members": "Apenas visível para participantes da comunidade",
"Filter community rooms": "Filtrar salas da comunidade",
"Filter community rooms": "Pesquisar salas da comunidade",
"Something went wrong when trying to get your communities.": "Não foi possível carregar suas comunidades.",
"Display your community flair in rooms configured to show it.": "Mostrar o ícone da sua comunidade nas salas que o permitem.",
"You're not currently a member of any communities.": "No momento, você não é participante de nenhuma comunidade.",
@ -2772,5 +2772,92 @@
"Uzbekistan": "Uzbequistão",
"Role": "Função",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "Armazene localmente com segurança as mensagens criptografadas para que apareçam nos resultados da pesquisa, usando %(size)s para armazenar mensagens de %(count)s sala.",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "Armazene localmente com segurança as mensagens criptografadas para que apareçam nos resultados da pesquisa, usando %(size)s para armazenar mensagens de %(count)s salas."
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "Armazene localmente com segurança as mensagens criptografadas para que apareçam nos resultados da pesquisa, usando %(size)s para armazenar mensagens de %(count)s salas.",
"Filter": "Pesquisar",
"Start a new chat": "Iniciar uma 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": "Armazene mensagens criptografadas de forma segura localmente para que apareçam nos resultados das buscas. %(size)s é necessário para armazenar mensagens de %(rooms)s sala.",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Armazene mensagens criptografadas de forma segura localmente para que apareçam nos resultados das buscas. %(size)s é necessário para armazenar mensagens de %(rooms)s salas.",
"Filter rooms and people": "Pesquisar salas e pessoas",
"Open the link in the email to continue registration.": "Abra o link no e-mail para continuar o registro.",
"A confirmation email has been sent to %(emailAddress)s": "Um e-mail de confirmação foi enviado para %(emailAddress)s",
"Go to Home View": "Ir para a tela inicial",
"<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ágina da sua comunidade</h1>\n<p>\n Escreva uma descrição longa para apresentar novos membros à comunidade, ou liste\n alguns <a href=\"foo\">links</a> importantes\n</p>\n<p>\n Você pode até adicionar fotos com URLs na Matrix <img src=\"mxc://url\" />\n</p>\n",
"Remain on your screen while running": "Permaneça na tela, quando executar",
"Remain on your screen when viewing another room, when running": "Permaneça na tela ao visualizar outra sala, quando executar",
"New here? <a>Create an account</a>": "Novo por aqui? <a>Crie uma conta</a>",
"Got an account? <a>Sign in</a>": "Já tem uma conta? <a>Login</a>",
"Use Command + Enter to send a message": "Usar Command + Enter para enviar uma mensagem",
"Enter phone number": "Digite o número de telefone",
"Enter email address": "Digite o endereço de e-mail",
"Decline All": "Recusar tudo",
"Approve": "Autorizar",
"This widget would like to:": "Este widget gostaria de:",
"Approve widget permissions": "Autorizar as permissões do widget",
"Return to call": "Retornar para a chamada",
"Fill Screen": "Preencher a tela",
"Voice Call": "Chamada de voz",
"Video Call": "Chamada de vídeo",
"Use Ctrl + Enter to send a message": "Usar Ctrl + Enter para enviar uma mensagem",
"Render LaTeX maths in messages": "Renderizar fórmulas matemáticas LaTeX em mensagens",
"See <b>%(msgtype)s</b> messages posted to your active room": "Veja mensagens de <b>%(msgtype)s</b> enviadas nesta sala ativa",
"See <b>%(msgtype)s</b> messages posted to this room": "Veja mensagens de <b>%(msgtype)s</b> enviadas nesta sala",
"Send <b>%(msgtype)s</b> messages as you in your active room": "Enviar mensagens de <b>%(msgtype)s</b> nesta sala ativa",
"Send <b>%(msgtype)s</b> messages as you in this room": "Enviar mensagens de <b>%(msgtype)s</b> nesta sala",
"See general files posted to your active room": "Veja os arquivos enviados nesta sala ativa",
"See general files posted to this room": "Veja os arquivos enviados nesta sala",
"Send general files as you in your active room": "Enviar arquivos nesta sala ativa",
"Send general files as you in this room": "Enviar arquivos nesta sala",
"See videos posted to your active room": "Veja os vídeos enviados nesta sala ativa",
"See videos posted to this room": "Veja os vídeos enviados nesta sala",
"Send videos as you in your active room": "Enviar vídeos nesta sala ativa",
"Send videos as you in this room": "Enviar vídeos nesta sala",
"See images posted to your active room": "Veja as fotos enviadas nesta sala ativa",
"See images posted to this room": "Veja as fotos enviadas nesta sala",
"Send images as you in your active room": "Enviar fotos nesta sala ativa",
"Send images as you in this room": "Enviar fotos nesta sala",
"See emotes posted to your active room": "Veja emojis enviados nesta sala ativa",
"See emotes posted to this room": "Veja emojis enviados nesta sala",
"Send emotes as you in your active room": "Enviar emojis nesta sala ativa",
"Send emotes as you in this room": "Enviar emojis nesta sala",
"See text messages posted to your active room": "Veja as mensagens de texto enviadas nesta sala ativa",
"See text messages posted to this room": "Veja as mensagens de texto enviadas nesta sala",
"Send text messages as you in your active room": "Enviar mensagens de texto nesta sala ativa",
"Send text messages as you in this room": "Enviar mensagens de texto nesta sala",
"See messages posted to your active room": "Veja as mensagens enviadas nesta sala ativa",
"See messages posted to this room": "Veja as mensagens enviadas nesta sala",
"Send messages as you in your active room": "Enviar mensagens nesta sala ativa",
"Send messages as you in this room": "Enviar mensagens nesta sala",
"The <b>%(capability)s</b> capability": "A permissão <b>%(capability)s</b>",
"See <b>%(eventType)s</b> events posted to your active room": "Veja eventos de <b>%(eventType)s</b> enviados nesta sala ativa",
"Send <b>%(eventType)s</b> events as you in your active room": "Enviar eventos de <b>%(eventType)s</b> nesta sala ativa",
"See <b>%(eventType)s</b> events posted to this room": "Veja eventos de <b>%(eventType)s</b> postados nesta sala",
"Send <b>%(eventType)s</b> events as you in this room": "Enviar eventos de <b>%(eventType)s</b> nesta sala",
"with state key %(stateKey)s": "com chave de estado %(stateKey)s",
"with an empty state key": "com uma chave de estado vazia",
"See when anyone posts a sticker to your active room": "Veja quando alguém enviar uma figurinha nesta sala ativa",
"Send stickers to your active room as you": "Enviar figurinhas para esta sala ativa",
"See when a sticker is posted in this room": "Veja quando uma figurinha for enviada nesta sala",
"Send stickers to this room as you": "Enviar figurinhas para esta sala",
"See when the avatar changes in your active room": "Veja quando a foto desta sala ativa for alterada",
"Change the avatar of your active room": "Alterar a foto desta sala ativa",
"See when the avatar changes in this room": "Veja quando a foto desta sala for alterada",
"Change the avatar of this room": "Alterar a foto desta sala",
"See when the name changes in your active room": "Veja quando o nome desta sala ativa for alterado",
"Change the name of your active room": "Alterar o nome desta sala ativa",
"See when the name changes in this room": "Veja quando o nome desta sala for alterado",
"Change the name of this room": "Alterar o nome desta sala",
"See when the topic changes in your active room": "Veja quando a descrição for alterada nesta sala ativa",
"Change the topic of your active room": "Alterar a descrição desta sala ativa",
"See when the topic changes in this room": "Veja quando a descrição for alterada nesta sala",
"Change the topic of this room": "Alterar a descrição desta sala",
"Change which room you're viewing": "Alterar a sala que você está vendo",
"Send stickers into your active room": "Enviar figurinhas nesta sala ativa",
"Send stickers into this room": "Enviar figurinhas nesta sala",
"No other application is using the webcam": "Nenhum outro aplicativo está usando a câmera",
"Permission is granted to use the webcam": "Permissão concedida para usar a câmera",
"A microphone and webcam are plugged in and set up correctly": "Um microfone e uma câmera estão conectados e configurados corretamente",
"Call failed because no webcam or microphone could not be accessed. Check that:": "A chamada falhou porque não foi possível acessar alguma câmera ou microfone. Verifique se:",
"Unable to access webcam / microphone": "Não é possível acessar a câmera/microfone",
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "A chamada falhou porque não foi possível acessar algum microfone. Verifique se o microfone está conectado e configurado corretamente.",
"Unable to access microphone": "Não é possível acessar o microfone"
}

View file

@ -2558,5 +2558,52 @@
"Rate %(brand)s": "Оценить %(brand)s",
"Feedback sent": "Отзыв отправлен",
"%(senderName)s ended the call": "%(senderName)s завершил(а) звонок",
"You ended the call": "Вы закончили звонок"
"You ended the call": "Вы закончили звонок",
"Send stickers into this room": "Отправить стикеры в эту комнату",
"Use Ctrl + Enter to send a message": "Используйте Ctrl + Enter, чтобы отправить сообщение",
"Use Command + Enter to send a message": "Используйте Command + Enter, чтобы отправить сообщение",
"Go to Home View": "Перейти на главную страницу",
"Filter rooms and people": "Фильтровать комнаты и людей",
"Open the link in the email to continue registration.": "Откройте ссылку в письме, чтобы продолжить регистрацию.",
"A confirmation email has been sent to %(emailAddress)s": "Письмо с подтверждением отправлено на %(emailAddress)s",
"Start a new chat": "Начать новый чат",
"Role": "Роль",
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Сообщения в этой комнате полностью зашифрованы. Когда люди присоединяются, вы можете проверить их в их профиле, просто нажмите на их аватар.",
"This is the start of <roomName/>.": "Это начало <roomName/>.",
"Add a photo, so people can easily spot your room.": "Добавьте фото, чтобы люди могли легко заметить вашу комнату.",
"%(displayName)s created this room.": "%(displayName)s создал(а) эту комнату.",
"You created this room.": "Вы создали эту комнату.",
"<a>Add a topic</a> to help people know what it is about.": "<a>Добавьте тему</a>, чтобы люди знали, о чём комната.",
"Topic: %(topic)s ": "Тема: %(topic)s ",
"Topic: %(topic)s (<a>edit</a>)": "Тема: %(topic)s (<a>изменить</a>)",
"This is the beginning of your direct message history with <displayName/>.": "Это начало вашей истории прямых сообщений с <displayName/>.",
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "В этом разговоре только вы двое, если только кто-нибудь из вас не пригласит кого-нибудь присоединиться.",
"Takes the call in the current room off hold": "Прекратить удержание вызова в текущей комнате",
"Places the call in the current room on hold": "Перевести вызов в текущей комнате на удержание",
"Now, let's help you get started": "Теперь давайте поможем вам начать",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Пригласите кого-нибудь, используя его имя, адрес электронной почты, имя пользователя (например, <userId/>) или <a>поделитесь этой комнатой</a>.",
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Начните разговор с кем-нибудь, используя его имя, адрес электронной почты или имя пользователя (например, <userId/>).",
"Invite by email": "Пригласить по электронной почте",
"Welcome %(name)s": "Добро пожаловать, %(name)s",
"Add a photo so people know it's you.": "Добавьте фото, чтобы люди знали, что это вы.",
"Great, that'll help people know it's you": "Отлично, это поможет людям узнать, что это ты",
"Use the + to make a new room or explore existing ones below": "Используйте +, чтобы создать новую комнату или изучить существующие ниже",
"New version of %(brand)s is available": "Доступна новая версия %(brand)s!",
"Update %(brand)s": "Обновление %(brand)s",
"Enable desktop notifications": "Включить уведомления на рабочем столе",
"Don't miss a reply": "Не пропустите ответ",
"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": "Нет доступа к микрофону",
"Video Call": "Видеовызов",
"Voice Call": "Голосовой вызов",
"Fill Screen": "Заполнить экран",
"Return to call": "Вернуться к звонку",
"Got an account? <a>Sign in</a>": "Есть учётная запись? <a>Войти</a>",
"New here? <a>Create an account</a>": "Впервые здесь? <a>Создать учётную запись</a>",
"Render LaTeX maths in messages": "Отображать математику LaTeX в сообщениях"
}

View file

@ -2834,5 +2834,90 @@
"Mauritania": "Mauritani",
"Bangladesh": "Bangladesh",
"Falkland Islands": "Ishujt Falkland",
"Sweden": "Suedi"
"Sweden": "Suedi",
"Filter rooms and people": "Filtroni dhoma dhe njerëz",
"Open the link in the email to continue registration.": "Që të vazhdohet regjistrimi, hapni lidhjen te email-i.",
"A confirmation email has been sent to %(emailAddress)s": "Te %(emailAddress)s u dërgua një email ripohimi",
"Role": "Rol",
"Start a new chat": "Nisni një fjalosje të re",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Ruajini lokalisht në fshehtinë në mënyrë të sigurt mesazhet e fshehtëzuar, që të shfaqen në përfundime kërkimi, duke përdorur %(size)s që të depozitoni mesazhe nga %(rooms)s dhomë.",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Ruajini lokalisht në fshehtinë në mënyrë të sigurt mesazhet e fshehtëzuar, që të shfaqen në përfundime kërkimi, duke përdorur %(size)s që të depozitoni mesazhe nga %(rooms)s dhoma.",
"See emotes posted to your active room": "Shihni emotikonë postuar në dhomën tuaj aktive",
"See emotes posted to this room": "Shihni emotikone postuar në këtë dhomë",
"Send emotes as you in your active room": "Dërgoni emotikone si ju në këtë dhomë",
"Send emotes as you in this room": "Dërgoni emotikone si ju në këtë dhomë",
"See text messages posted to your active room": "Shihni mesazhe tekst postuar në dhomën tuaj aktive",
"See text messages posted to this room": "Shihni mesazhe tekst postuar në këtë dhomë",
"Send text messages as you in your active room": "Dërgoni mesazhe tekst si ju në dhomën tuaj aktive",
"Send text messages as you in this room": "Dërgoni mesazhe tekst si ju në këtë dhomë",
"See messages posted to your active room": "Shihni mesazhe të postuar në dhomën tuaj aktive",
"See messages posted to this room": "Shihni mesazhe të postuar në këtë dhomë",
"Send messages as you in your active room": "Dërgoni mesazhe si ju në dhomën tuaj aktive",
"Send messages as you in this room": "Dërgoni mesazhi si ju në këtë dhomë",
"The <b>%(capability)s</b> capability": "Aftësia <b>%(capability)s</b>",
"See <b>%(eventType)s</b> events posted to your active room": "Shihni akte <b>%(eventType)s</b> postuar në dhomën tuaj aktive",
"Send <b>%(eventType)s</b> events as you in your active room": "Shihni akte <b>%(eventType)s</b> si ju në këtë dhomë",
"See <b>%(eventType)s</b> events posted to this room": "Shihni akte <b>%(eventType)s</b> postuar në këtë dhomë",
"Send <b>%(eventType)s</b> events as you in this room": "Dërgoni akte <b>%(eventType)s</b> në këtë dhomë si ju",
"with state key %(stateKey)s": "me kyç gjendjeje %(stateKey)s",
"with an empty state key": "me një kyç të zbrazët gjendjeje",
"See when anyone posts a sticker to your active room": "Shihni kur dikush poston një ngjitës në dhomën tuaj aktive",
"Send stickers to your active room as you": "Dërgoni ngjitës në dhomën tuaj aktive si ju",
"See when a sticker is posted in this room": "Shihni kur postohet një ngjitës në këtë dhomë",
"Send stickers to this room as you": "Dërgoni ngjitës në këtë dhomë si ju",
"See when the avatar changes in your active room": "Shihni kur ndryshon avatari në dhomën tuaj aktive",
"Change the avatar of your active room": "Ndryshoni avatarin në dhomën tuaj aktive",
"See when the avatar changes in this room": "Shihni kur ndryshon avatari në këtë dhomë",
"Change the avatar of this room": "Ndryshoni avatarin e kësaj dhome",
"See when the name changes in your active room": "Shihni kur ndryshon emri në dhomën tuaj aktive",
"Change the name of your active room": "Ndryshoni emrin e dhomës tuaj aktive",
"See when the name changes in this room": "Shihni kur ndryshohet emri në këtë dhomë",
"Change the name of this room": "Ndryshoni emrin e kësaj dhome",
"See when the topic changes in your active room": "Shihni kur ndryshon tema në dhomën tuaj aktive",
"Change the topic of your active room": "Ndryshoni temën në dhomën tuaj aktive",
"See when the topic changes in this room": "Shihni kur ndryshohet tema në këtë dhomë",
"Change the topic of this room": "Ndryshoni temën e kësaj dhome",
"Change which room you're viewing": "Ndryshoni cilën dhomë shihni",
"Send stickers into your active room": "Dërgoni ngjitës në dhomën tuaj aktive",
"Send stickers into this room": "Dërgoni ngjitës në këtë dhomë",
"Go to Home View": "Kaloni te Pamja Kreu",
"<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 për faqen e bashkësisë tuaj</h1>\n<p>\n Përdoreni përshkrimin e gjatë që ti prezantoni bashkësisë anëtarë të rinj, ose për tu dhënë <a href=\"foo\">lidhje</a> të rëndësishme\n</p>\n<p>\n Mundeni madje të shtoni figura me URL-ra Matrix <img src=\"mxc://url\" />\n</p>\n",
"Enter phone number": "Jepni numër telefoni",
"Enter email address": "Jepni adresë email-i",
"Decline All": "Hidhi Krejt Poshtë",
"Approve": "Miratojeni",
"This widget would like to:": "Ky widget do të donte të:",
"Approve widget permissions": "Miratoni leje widget-i",
"Use Ctrl + Enter to send a message": "Që të dërgoni një mesazh përdorni tastet Ctrl + Enter",
"Use Command + Enter to send a message": "Që të dërgoni një mesazh, përdorni tastet Command + Enter",
"See <b>%(msgtype)s</b> messages posted to your active room": "Shihni mesazhe <b>%(msgtype)s</b> postuar në dhomën tuaj aktive",
"See <b>%(msgtype)s</b> messages posted to this room": "Shihni mesazhe <b>%(msgtype)s</b> postuar në këtë dhomë",
"Send <b>%(msgtype)s</b> messages as you in your active room": "Dërgoni mesazhe <b>%(msgtype)s</b> si ju në dhomën tuaj aktive",
"Send <b>%(msgtype)s</b> messages as you in this room": "Dërgoni mesazhe <b>%(msgtype)s</b> si ju në këtë dhomë",
"See general files posted to your active room": "Shihni kartela të përgjithshme postuar në dhomën tuaj aktive",
"See general files posted to this room": "Shihni kartela të përgjithshme postuar në këtë dhomë",
"Send general files as you in your active room": "Dërgoni kartela të përgjithshme si ju në dhomën tuaj aktive",
"Send general files as you in this room": "Dërgoni kartela të përgjithshme si ju në këtë dhomë",
"See videos posted to your active room": "Shihni video të postuara në dhomën tuaj aktive",
"See videos posted to this room": "Shihni video të postuara në këtë dhomë",
"Send videos as you in your active room": "Dërgoni video si ju në dhomën tuaj aktive",
"Send videos as you in this room": "Dërgoni video si ju në këtë dhomë",
"See images posted to your active room": "Shihni figura postuar te dhoma juaj aktive",
"See images posted to this room": "Shihni figura postuar në këtë dhomë",
"Send images as you in your active room": "Dërgoni figura si ju në dhomën tuaj aktive",
"New here? <a>Create an account</a>": "I sapoardhur? <a>Krijoni një llogari</a>",
"Got an account? <a>Sign in</a>": "Keni një llogari? <a>Hyni</a>",
"Return to call": "Kthehu te thirrja",
"Fill Screen": "Mbushe Ekranin",
"Voice Call": "Thirrje Zanore",
"Video Call": "Thirrje Video",
"Render LaTeX maths in messages": "Formo formula LaTeX në mesazhe",
"Send images as you in this room": "Dërgoni figura si ju, në këtë dhomë",
"No other application is using the webcam": "Kamerën spo e përdor aplikacion tjetër",
"Permission is granted to use the webcam": "Është dhënë leje për përdorimin e kamerës",
"A microphone and webcam are plugged in and set up correctly": "Një mikrofon dhe një kamerë janë futur dhe ujdisur si duhet",
"Call failed because no webcam or microphone could not be accessed. Check that:": "Thirrja dështoi, ngaqë su bë dot hyrje në kamerë ose mikrofon. Kontrolloni që:",
"Unable to access webcam / microphone": "Sarrihet të përdoret kamerë / mikrofon",
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Thirrja dështoi, ngaqë su hap dot ndonjë mikrofon. Shihni që të jetë futur një mikrofon dhe ujdiseni saktë.",
"Unable to access microphone": "Sarrihet të përdoret mikrofoni"
}

View file

@ -2774,5 +2774,14 @@
"Central African Republic": "Centralafrikanska republiken",
"Cayman Islands": "Caymanöarna",
"Caribbean Netherlands": "Karibiska Nederländerna",
"Cape Verde": "Kap Verde"
"Cape Verde": "Kap Verde",
"Change which room you're viewing": "Ändra vilket rum du visar",
"Send stickers into your active room": "Skicka dekaler in i ditt aktiva rum",
"Send stickers into this room": "Skicka dekaler in i det här rummet",
"Remain on your screen while running": "Stanna kvar på skärmen när det körs",
"Remain on your screen when viewing another room, when running": "Stanna kvar på skärmen när ett annat rum visas, när det körs",
"See when the topic changes in this room": "Se när ämnet ändras i det här rummet",
"Change the topic of this room": "Ändra ämnet för det här rummet",
"See when the topic changes in your active room": "Se när ämnet ändras i ditt aktiva rum",
"Change the topic of your active room": "Ändra ämnet för ditt aktiva rum"
}

View file

@ -1264,5 +1264,264 @@
"The other party cancelled the verification.": "Друга сторона скасувала звірення.",
"Verified!": "Звірено!",
"You've successfully verified this user.": "Ви успішно звірили цього користувача.",
"Got It": "Зрозуміло"
"Got It": "Зрозуміло",
"Comoros": "Коморські Острови",
"Colombia": "Колумбія",
"Cocos (Keeling) Islands": "Кокосові (Кілінг) Острови",
"Christmas Island": "Острів Різдва",
"China": "Китай",
"Chile": "Чилі",
"Chad": "Чад",
"Central African Republic": "Центральноафриканська Республіка",
"Cayman Islands": "Кайманові Острови",
"Caribbean Netherlands": "Бонайре, Сінт-Естатіус і Саба",
"Cape Verde": "Кабо-Верде",
"Canada": "Канада",
"Cameroon": "Камерун",
"Cambodia": "Камбоджа",
"Burundi": "Бурунді",
"Burkina Faso": "Буркіна-Фасо",
"Bulgaria": "Болгарія",
"Brunei": "Бруней",
"British Virgin Islands": "Британські Віргінські Острови",
"British Indian Ocean Territory": "Британська територія в Індійському океані",
"Brazil": "Бразилія",
"Bouvet Island": "Острів Буве",
"Botswana": "Ботсвана",
"Bosnia": "Боснія і Герцеговина",
"Bolivia": "Болівія",
"Bhutan": "Бутан",
"Bermuda": "Бермудські Острови",
"Benin": "Бенін",
"Belize": "Беліз",
"Belgium": "Бельгія",
"Belarus": "Білорусь",
"Barbados": "Барбадос",
"Bangladesh": "Бангладеш",
"Bahrain": "Бахрейн",
"Bahamas": "Багамські Острови",
"Azerbaijan": "Азербайджан",
"Austria": "Австрія",
"Australia": "Австралія",
"Aruba": "Аруба",
"Armenia": "Вірменія",
"Argentina": "Аргентина",
"Antigua & Barbuda": "Антигуа і Барбуда",
"Antarctica": "Антарктика",
"Anguilla": "Ангілья",
"Angola": "Ангола",
"Andorra": "Андорра",
"American Samoa": "Американське Самоа",
"Algeria": "Алжир",
"Albania": "Албанія",
"Åland Islands": "Аландські Острови",
"Afghanistan": "Афганістан",
"United States": "Сполучені Штати Америки",
"United Kingdom": "Велика Британія",
"The call was answered on another device.": "На дзвінок відповіли на іншому пристрої.",
"Answered Elsewhere": "Відповіли деінде",
"The call could not be established": "Не вдалося встановити зв'язок",
"The other party declined the call.": "Інша сторона відхилила дзвінок.",
"Call Declined": "Дзвінок відхилено",
"Falkland Islands": "Фолклендські (Мальвінські) Острови",
"Ethiopia": "Ефіопія",
"Estonia": "Естонія",
"Eritrea": "Еритрея",
"Equatorial Guinea": "Екваторіальна Гвінея",
"El Salvador": "Сальвадор",
"Egypt": "Єгипет",
"Ecuador": "Еквадор",
"Dominican Republic": "Домініканська Республіка",
"Dominica": "Домініка",
"Djibouti": "Джибуті",
"Denmark": "Данія",
"Côte dIvoire": "Кот-Д'Івуар",
"Czech Republic": "Чехія",
"Cyprus": "Кіпр",
"Curaçao": "Кюрасао",
"Cuba": "Куба",
"Croatia": "Хорватія",
"Costa Rica": "Коста-Рика",
"Cook Islands": "Острови Кука",
"Congo - Kinshasa": "Демократична Республіка Конго",
"Congo - Brazzaville": "Конго",
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s змінив серверні права доступу для цієї кімнати.",
"%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s встановив серверні права доступу для цієї кімнати",
"Takes the call in the current room off hold": "Зніміть дзвінок у поточній кімнаті з утримання",
"Places the call in the current room on hold": "Переведіть дзвінок у поточній кімнаті на утримання",
"Zimbabwe": "Зімбабве",
"Zambia": "Замбія",
"Yemen": "Ємен",
"Western Sahara": "Західна Сахара",
"Wallis & Futuna": "Волліс і Футуна",
"Vietnam": "В'єтнам",
"Venezuela": "Венесуела",
"Vatican City": "Ватикан",
"Vanuatu": "Вануату",
"Uzbekistan": "Узбекистан",
"Uruguay": "Уругвай",
"United Arab Emirates": "Об'єднані Арабські Емірати",
"Ukraine": "Україна",
"Uganda": "Уганда",
"U.S. Virgin Islands": "Віргінські Острови (США)",
"Tuvalu": "Тувалу",
"Turks & Caicos Islands": "Острови Теркс і Кайкос",
"Turkmenistan": "Туркменистан",
"Turkey": "Туреччина",
"Tunisia": "Туніс",
"Trinidad & Tobago": "Тринідад і Тобаго",
"Tonga": "Тонга",
"Tokelau": "Токелау",
"Togo": "Того",
"Timor-Leste": "Тимор-Лешті",
"Thailand": "Таїланд",
"Tanzania": "Танзанія",
"Tajikistan": "Таджикистан",
"Taiwan": "Тайвань",
"São Tomé & Príncipe": "Сан-Томе і Принсіпі",
"Syria": "Сирія",
"Switzerland": "Швейцарія",
"Sweden": "Швеція",
"Swaziland": "Есватіні",
"Svalbard & Jan Mayen": "Свальбард і Ян-Маєн",
"Suriname": "Суринам",
"Sudan": "Судан",
"St. Vincent & Grenadines": "Сент-Вінсент і Гренадини",
"St. Pierre & Miquelon": "Сен-П'єр і Мікелон",
"St. Martin": "Сен-Мартен",
"St. Lucia": "Сент-Люсія",
"St. Kitts & Nevis": "Сент-Кітс і Невіс",
"St. Helena": "Острів Святої Єлени",
"St. Barthélemy": "Сен-Бартелемі",
"Sri Lanka": "Шрі-Ланка",
"Spain": "Іспанія",
"South Sudan": "Південний Судан",
"South Korea": "Південна Корея",
"South Georgia & South Sandwich Islands": "Південна Джорджія та Південні Сандвічеві Острови",
"South Africa": "Південна Африка",
"Somalia": "Сомалі",
"Solomon Islands": "Соломонові Острови",
"Slovenia": "Словенія",
"Slovakia": "Словаччина",
"Sint Maarten": "Сінт-Мартен",
"Singapore": "Сингапур",
"Sierra Leone": "Сьєрра-Леоне",
"Seychelles": "Сейшельські Острови",
"Serbia": "Сербія",
"Senegal": "Сенегал",
"Saudi Arabia": "Саудівська Аравія",
"San Marino": "Сан-Марино",
"Samoa": "Самоа",
"Réunion": "Реюньйон",
"Rwanda": "Руанда",
"Russia": "Російська Федерація",
"Romania": "Румунія",
"Qatar": "Катар",
"Puerto Rico": "Пуерто-Рико",
"Portugal": "Португалія",
"Poland": "Польща",
"Pitcairn Islands": "Піткерн",
"Philippines": "Філіппіни",
"Peru": "Перу",
"Paraguay": "Парагвай",
"Papua New Guinea": "Папуа-Нова Гвінея",
"Panama": "Панама",
"Palestine": "Палестина",
"Palau": "Палау",
"Pakistan": "Пакистан",
"Oman": "Оман",
"Norway": "Норвегія",
"Northern Mariana Islands": "Північні Маріанські Острови",
"North Korea": "Північна Корея",
"Norfolk Island": "Острів Норфолк",
"Niue": "Ніуе",
"Nigeria": "Нігерія",
"Niger": "Нігер",
"Nicaragua": "Нікарагуа",
"New Zealand": "Нова Зеландія",
"New Caledonia": "Нова Каледонія",
"Netherlands": "Нідерланди",
"Nepal": "Непал",
"Nauru": "Науру",
"Namibia": "Намібія",
"Myanmar": "М'янма",
"Mozambique": "Мозамбік",
"Morocco": "Марокко",
"Montserrat": "Монтсеррат",
"Montenegro": "Чорногорія",
"Mongolia": "Монголія",
"Monaco": "Монако",
"Moldova": "Молдова",
"Micronesia": "Мікронезія",
"Mexico": "Мексика",
"Mayotte": "Майотта",
"Mauritius": "Маврикій",
"Mauritania": "Мавританія",
"Martinique": "Мартиніка",
"Marshall Islands": "Маршаллові Острови",
"Malta": "Мальта",
"Mali": "Малі",
"Maldives": "Мальдіви",
"Malaysia": "Малайзія",
"Malawi": "Малаві",
"Madagascar": "Мадагаскар",
"Macedonia": "Північна Македонія",
"Macau": "Макао",
"Luxembourg": "Люксембург",
"Lithuania": "Литва",
"Liechtenstein": "Ліхтенштейн",
"Libya": "Лівія",
"Liberia": "Ліберія",
"Lesotho": "Лесото",
"Lebanon": "Ліван",
"Latvia": "Латвія",
"Laos": "Лаоська Народно-Демократична Республіка",
"Kyrgyzstan": "Киргизстан",
"Kuwait": "Кувейт",
"Kosovo": "Косово",
"Kiribati": "Кірибаті",
"Kenya": "Кенія",
"Kazakhstan": "Казахстан",
"Jordan": "Йорданія",
"Jersey": "Джерсі",
"Japan": "Японія",
"Jamaica": "Ямайка",
"Italy": "Італія",
"Israel": "Ізраїль",
"Isle of Man": "Острів Мен",
"Ireland": "Ірландія",
"Iraq": "Ірак",
"Iran": "Іран",
"Indonesia": "Індонезія",
"India": "Індія",
"Iceland": "Ісландія",
"Hungary": "Угорщина",
"Hong Kong": "Гонконг",
"Honduras": "Гондурас",
"Heard & McDonald Islands": "Острів Герд і Острови Макдоналд",
"Haiti": "Гаїті",
"Guyana": "Гаяна",
"Guinea-Bissau": "Гвінея-Бісау",
"Guinea": "Гвінея",
"Guernsey": "Гернсі",
"Guatemala": "Гватемала",
"Guam": "Гуам",
"Guadeloupe": "Гваделупа",
"Grenada": "Гренада",
"Greenland": "Гренландія",
"Greece": "Греція",
"Gibraltar": "Гібралтар",
"Ghana": "Гана",
"Germany": "Німеччина",
"Georgia": "Грузія",
"Gambia": "Гамбія",
"Gabon": "Габон",
"French Southern Territories": "Французькі Південні Території",
"French Polynesia": "Французька Полінезія",
"French Guiana": "Французька Гвіана",
"France": "Франція",
"Finland": "Фінляндія",
"Fiji": "Фіджі",
"Faroe Islands": "Фарерські Острови",
"Unable to access microphone": "Неможливо доступитись до мікрофона"
}

View file

@ -2398,5 +2398,16 @@
"Privacy": "隐私",
"Explore community rooms": "探索社区聊天室",
"%(count)s results|one": "%(count)s 个结果",
"Room Info": "聊天室信息"
"Room Info": "聊天室信息",
"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": "无法使用麦克风",
"The call was answered on another device.": "在另一台设备上应答了该通话。",
"The call could not be established": "无法建立通话",
"The other party declined the call.": "对方拒绝了通话。",
"Call Declined": "通话被拒绝"
}

View file

@ -2844,5 +2844,91 @@
"Places the call in the current room on hold": "在目前的聊天室撥打通話並等候接聽",
"Role": "角色",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "使用 %(size)s 儲存來自 %(count)s 個聊天室的訊息,在本機安全地快取已加密的訊息以讓它們可以在搜尋結果中出現。",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "使用 %(size)s 儲存來自 %(count)s 個聊天室的訊息,在本機安全地快取已加密的訊息以讓它們可以在搜尋結果中出現。"
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "使用 %(size)s 儲存來自 %(count)s 個聊天室的訊息,在本機安全地快取已加密的訊息以讓它們可以在搜尋結果中出現。",
"Go to Home View": "轉到主視窗",
"Filter rooms and people": "過濾聊天室與人們",
"Open the link in the email to continue registration.": "開啟電子郵件中的連結以繼續註冊。",
"A confirmation email has been sent to %(emailAddress)s": "確認電子郵件已寄送至 %(emailAddress)s",
"Start a new chat": "開始新聊天",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "使用 %(size)s 來儲存來自 %(rooms)s 個聊天室的訊息,在本機安全地快取已加密的訊息以使其出現在搜尋結果中。",
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "使用 %(size)s 來儲存來自 %(rooms)s 個聊天室的訊息,在本機安全地快取已加密的訊息以使其出現在搜尋結果中。",
"<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</h1>\n<p>\n 使用詳細說明向社群介紹新成員,或散佈\n 一些重要的<a href=\"foo\">連結</a>\n</p>\n<p>\n 您甚至可以使用 Matrix URL <img src=\"mxc://url\" /> 新增圖片\n</p>\n",
"Decline All": "全部拒絕",
"Approve": "批准",
"This widget would like to:": "這個小工具想要:",
"Approve widget permissions": "批准小工具權限",
"Use Ctrl + Enter to send a message": "使用 Ctrl + Enter 來傳送訊息",
"Use Command + Enter to send a message": "使用 Command + Enter 來傳送訊息",
"See <b>%(msgtype)s</b> messages posted to your active room": "檢視發佈到您的活躍聊天室的 <b>%(msgtype)s</b> 訊息",
"See <b>%(msgtype)s</b> messages posted to this room": "檢視發佈到此聊天室的 <b>%(msgtype)s</b> 訊息",
"Send <b>%(msgtype)s</b> messages as you in your active room": "在您的活躍聊天室中以您的身份傳送 <b>%(msgtype)s</b> 訊息",
"Send <b>%(msgtype)s</b> messages as you in this room": "在此聊天室中以您的身份傳送 <b>%(msgtype)s</b> 訊息",
"See general files posted to your active room": "檢視在您的活躍聊天室中發佈的一般檔案",
"See general files posted to this room": "檢視在此聊天室中發佈的一般檔案",
"Send general files as you in your active room": "在您的活躍聊天室中以您的身份傳送一般檔案",
"Send general files as you in this room": "在此聊天室中以您的身份傳送一般檔案",
"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 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": "檢視發佈到您的活躍聊天室的訊息",
"See messages posted to this 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": "在執行與檢視其他聊天室時仍保留在您的畫面上",
"Enter phone number": "輸入電話號碼",
"Enter email address": "輸入電子郵件地址",
"Return to call": "回到通話",
"Fill Screen": "全螢幕",
"Voice Call": "音訊通話",
"Video Call": "視訊通話",
"New here? <a>Create an account</a>": "新手?<a>建立帳號</a>",
"Got an account? <a>Sign in</a>": "有帳號了嗎?<a>登入</a>",
"Render LaTeX maths in messages": "在訊息中彩現 LaTeX 數學",
"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": "無法存取麥克風"
}

View file

@ -29,6 +29,8 @@ import WidgetUtils from "../utils/WidgetUtils";
import {SettingLevel} from "../settings/SettingLevel";
import {WidgetType} from "../widgets/WidgetType";
import {UPDATE_EVENT} from "./AsyncStore";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { arrayDiff, arrayHasDiff, arrayUnion } from "../utils/arrays";
interface IState {}
@ -39,20 +41,26 @@ export interface IApp extends IWidget {
avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
}
type PinnedWidgets = Record<string, boolean>;
interface IRoomWidgets {
widgets: IApp[];
pinned: Record<string, boolean>;
pinned: PinnedWidgets;
}
export const MAX_PINNED = 3;
function widgetUid(app: IApp): string {
return `${app.roomId ?? MatrixClientPeg.get().getUserId()}::${app.id}`;
}
// TODO consolidate WidgetEchoStore into this
// TODO consolidate ActiveWidgetStore into this
export default class WidgetStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new WidgetStore();
private widgetMap = new Map<string, IApp>();
private roomMap = new Map<string, IRoomWidgets>();
private widgetMap = new Map<string, IApp>(); // Key is widget Unique ID (UID)
private roomMap = new Map<string, IRoomWidgets>(); // Key is room ID
private constructor() {
super(defaultDispatcher, {});
@ -126,13 +134,20 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
// first clean out old widgets from the map which originate from this room
// otherwise we are out of sync with the rest of the app with stale widget events during removal
Array.from(this.widgetMap.values()).forEach(app => {
if (app.roomId === room.roomId) {
this.widgetMap.delete(app.id);
}
this.widgetMap.delete(widgetUid(app));
});
this.generateApps(room).forEach(app => {
this.widgetMap.set(app.id, app);
// Sanity check for https://github.com/vector-im/element-web/issues/15705
const existingApp = this.widgetMap.get(widgetUid(app));
if (existingApp) {
console.warn(
`Possible widget ID conflict for ${app.id} - wants to store in room ${app.roomId} ` +
`but is currently stored as ${existingApp.roomId} - letting the want win`,
);
}
this.widgetMap.set(widgetUid(app), app);
roomInfo.widgets.push(app);
});
this.emit(room.roomId);
@ -146,61 +161,68 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
this.emit(UPDATE_EVENT);
};
public getRoomId = (widgetId: string) => {
const app = this.widgetMap.get(widgetId);
if (!app) return null;
return app.roomId;
}
public getRoom = (roomId: string) => {
return this.roomMap.get(roomId);
};
private onPinnedWidgetsChange = (settingName: string, roomId: string) => {
this.initRoom(roomId);
this.getRoom(roomId).pinned = SettingsStore.getValue(settingName, roomId);
const pinned: PinnedWidgets = SettingsStore.getValue(settingName, roomId);
// Sanity check for https://github.com/vector-im/element-web/issues/15705
const roomInfo = this.getRoom(roomId);
const remappedPinned: PinnedWidgets = {};
for (const widgetId of Object.keys(pinned)) {
const isPinned = pinned[widgetId];
if (!roomInfo.widgets?.some(w => w.id === widgetId)) {
console.warn(`Skipping pinned widget update for ${widgetId} in ${roomId} -- wrong room`);
} else {
remappedPinned[widgetId] = isPinned;
}
}
roomInfo.pinned = remappedPinned;
this.emit(roomId);
this.emit(UPDATE_EVENT);
};
public isPinned(widgetId: string) {
const roomId = this.getRoomId(widgetId);
public isPinned(roomId: string, widgetId: string) {
return !!this.getPinnedApps(roomId).find(w => w.id === widgetId);
}
public canPin(widgetId: string) {
const roomId = this.getRoomId(widgetId);
// dev note: we don't need the widgetId on this function, but the contract makes more sense
// when we require it.
public canPin(roomId: string, widgetId: string) {
return this.getPinnedApps(roomId).length < MAX_PINNED;
}
public pinWidget(widgetId: string) {
const roomId = this.getRoomId(widgetId);
public pinWidget(roomId: string, widgetId: string) {
const roomInfo = this.getRoom(roomId);
if (!roomInfo) return;
// When pinning, first confirm all the widgets (Jitsi) which were autopinned so that the order is correct
const autoPinned = this.getPinnedApps(roomId).filter(app => !roomInfo.pinned[app.id]);
autoPinned.forEach(app => {
this.setPinned(app.id, true);
this.setPinned(roomId, app.id, true);
});
this.setPinned(widgetId, true);
this.setPinned(roomId, widgetId, true);
// Show the apps drawer upon the user pinning a widget
if (RoomViewStore.getRoomId() === this.getRoomId(widgetId)) {
if (RoomViewStore.getRoomId() === roomId) {
defaultDispatcher.dispatch({
action: "appsDrawer",
show: true,
})
});
}
}
public unpinWidget(widgetId: string) {
this.setPinned(widgetId, false);
public unpinWidget(roomId: string, widgetId: string) {
this.setPinned(roomId, widgetId, false);
}
private setPinned(widgetId: string, value: boolean) {
const roomId = this.getRoomId(widgetId);
private setPinned(roomId: string, widgetId: string, value: boolean) {
const roomInfo = this.getRoom(roomId);
if (!roomInfo) return;
if (roomInfo.pinned[widgetId] === false && value) {
@ -221,9 +243,8 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
this.emit(UPDATE_EVENT);
}
public movePinnedWidget(widgetId: string, delta: 1 | -1) {
public movePinnedWidget(roomId: string, widgetId: string, delta: 1 | -1) {
// TODO simplify this by changing the storage medium of pinned to an array once the Jitsi default-on goes away
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId);
if (!roomInfo || roomInfo.pinned[widgetId] === false) return;
@ -264,12 +285,33 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
});
const order = Object.keys(roomInfo.pinned).filter(k => roomInfo.pinned[k]);
let apps = order.map(wId => this.widgetMap.get(wId)).filter(Boolean);
apps = apps.slice(0, priorityWidget ? MAX_PINNED - 1 : MAX_PINNED);
const apps = order
.map(wId => Array.from(this.widgetMap.values())
.find(w2 => w2.roomId === roomId && w2.id === wId))
.filter(Boolean)
.slice(0, priorityWidget ? MAX_PINNED - 1 : MAX_PINNED);
if (priorityWidget) {
apps.push(priorityWidget);
}
// Sanity check for https://github.com/vector-im/element-web/issues/15705
// We union the app IDs the above generated with the roomInfo's known widgets to
// get a list of IDs which both exist. We then diff that against the generated app
// IDs above to ensure that all of the app IDs are captured by the union with the
// room - if we grabbed a widget that wasn't part of the roomInfo's list, it wouldn't
// be in the union and thus result in a diff.
const appIds = apps.map(a => widgetUid(a));
const roomAppIds = roomInfo.widgets.map(a => widgetUid(a));
const roomAppIdsUnion = arrayUnion(appIds, roomAppIds);
const missingSomeApps = arrayHasDiff(roomAppIdsUnion, appIds);
if (missingSomeApps) {
const diff = arrayDiff(roomAppIdsUnion, appIds);
console.warn(
`${roomId} appears to have a conflict for which widgets belong to it. ` +
`Widget UIDs are: `, [...diff.added, ...diff.removed],
);
}
return apps;
}

View file

@ -140,6 +140,6 @@ export async function leaveRoomBehaviour(roomId: string) {
}
if (RoomViewStore.getRoomId() === roomId) {
dis.dispatch({action: 'view_next_room'});
dis.dispatch({action: 'view_home_page'});
}
}

View file

@ -52,7 +52,7 @@ describe('Login', function() {
// Set non-empty flows & matrixClient to get past the loading spinner
root.setState({
currentFlow: "m.login.password",
flows: [{ type: "m.login.password" }],
});
const form = ReactTestUtils.findRenderedComponentWithType(
@ -61,10 +61,7 @@ describe('Login', function() {
);
expect(form).toBeTruthy();
const changeServerLink = ReactTestUtils.findRenderedDOMComponentWithClass(
root,
'mx_AuthBody_editServerDetails',
);
const changeServerLink = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_ServerPicker_change');
expect(changeServerLink).toBeTruthy();
});
@ -77,7 +74,7 @@ describe('Login', function() {
// Set non-empty flows & matrixClient to get past the loading spinner
root.setState({
currentFlow: "m.login.password",
flows: [{ type: "m.login.password" }],
});
const form = ReactTestUtils.findRenderedComponentWithType(
@ -86,10 +83,70 @@ describe('Login', function() {
);
expect(form).toBeTruthy();
const changeServerLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(
root,
'mx_AuthBody_editServerDetails',
);
const changeServerLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, 'mx_ServerPicker_change');
expect(changeServerLinks).toHaveLength(0);
});
it("should show SSO button if that flow is available", () => {
jest.spyOn(SdkConfig, "get").mockReturnValue({
disable_custom_urls: true,
});
const root = render();
// Set non-empty flows & matrixClient to get past the loading spinner
root.setState({
flows: [{ type: "m.login.sso" }],
});
const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton");
expect(ssoButton).toBeTruthy();
});
it("should show both SSO button and username+password if both are available", () => {
jest.spyOn(SdkConfig, "get").mockReturnValue({
disable_custom_urls: true,
});
const root = render();
// Set non-empty flows & matrixClient to get past the loading spinner
root.setState({
flows: [{ type: "m.login.password" }, { type: "m.login.sso" }],
});
const form = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('auth.PasswordLogin'));
expect(form).toBeTruthy();
const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton");
expect(ssoButton).toBeTruthy();
});
it("should show multiple SSO buttons if multiple identity_providers are available", () => {
jest.spyOn(SdkConfig, "get").mockReturnValue({
disable_custom_urls: true,
});
const root = render();
// Set non-empty flows & matrixClient to get past the loading spinner
root.setState({
flows: [{
type: "m.login.sso",
identity_providers: [{
id: "a",
name: "Provider 1",
}, {
id: "b",
name: "Provider 2",
}, {
id: "c",
name: "Provider 3",
}],
}],
});
const ssoButtons = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, "mx_SSOButton");
expect(ssoButtons.length).toBe(3);
});
});

View file

@ -48,12 +48,9 @@ describe('Registration', function() {
/>, parentDiv);
}
it('should show server type selector', function() {
it('should show server picker', function() {
const root = render();
const selector = ReactTestUtils.findRenderedComponentWithType(
root,
sdk.getComponent('auth.ServerTypeSelector'),
);
const selector = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_ServerPicker");
expect(selector).toBeTruthy();
});
@ -79,4 +76,27 @@ describe('Registration', function() {
);
expect(form).toBeTruthy();
});
it("should show SSO options if those are available", () => {
jest.spyOn(SdkConfig, "get").mockReturnValue({
disable_custom_urls: true,
});
const root = render();
// Set non-empty flows & matrixClient to get past the loading spinner
root.setState({
flows: [{
stages: [],
}],
ssoFlow: {
type: "m.login.sso",
},
matrixClient: {},
busy: false,
});
const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton");
expect(ssoButton).toBeTruthy();
});
});

View file

@ -1,3 +1,3 @@
node_modules
*.png
riot/env
element/env

View file

@ -5,9 +5,9 @@ This directory contains tests for matrix-react-sdk. The tests fire up a headless
## Setup
Run `./install.sh`. This will:
- install Synapse, fetches the master branch at the moment. If anything fails here, please refer to the Synapse README to see if you're missing one of the prerequisites.
- install Riot, this fetches the master branch at the moment.
- install dependencies (will download copy of chrome)
- install Synapse, fetches the develop branch at the moment. If anything fails here, please refer to the Synapse README to see if you're missing one of the prerequisites.
- install Element Web, this fetches the develop branch at the moment.
- install dependencies (will download copy of Chrome)
## Running the tests
@ -15,7 +15,7 @@ Run tests with `./run.sh`.
### Debug tests locally.
`./run.sh` will run the tests against the Riot copy present in `riot/riot-web` served by a static Python HTTP server. You can symlink your `riot-web` develop copy here but that doesn't work well with Webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local Webpack server.
`./run.sh` will run the tests against the Element copy present in `element/element-web` served by a static Python HTTP server. You can symlink your `element-web` develop copy here but that doesn't work well with Webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local Webpack server.
```
./synapse/stop.sh && \
@ -26,8 +26,7 @@ It's important to always stop and start Synapse before each run of the tests to
start.js accepts these parameters (and more, see `node start.js --help`) that can help running the tests locally:
- `--riot-url <url>` don't use the Riot copy and static server provided by the tests, but use a running server like the Webpack watch server to run the tests against. Make sure to have the following local config:
- `welcomeUserId` disabled as the tests assume there is no riot-bot currently.
- `--app-url <url>` don't use the Element Web copy and static server provided by the tests, but use a running server like the Webpack watch server to run the tests against.
- `--slow-mo` type at a human speed, useful with `--windowed`.
- `--throttle-cpu <factor>` throttle cpu in the browser by the given factor. Useful to reproduce failures because of insufficient timeouts happening on the slower CI server.
- `--windowed` run the tests in an actual browser window Try to limit interacting with the windows while the tests are running. Hovering over the window tends to fail the tests, dragging the title bar should be fine though.

View file

@ -5,14 +5,14 @@ and start following these steps to get going:
1. Navigate to your working directory (`cd /mnt/c/users/travisr/whatever/matrix-react-sdk` for example).
2. Run `sudo apt-get install unzip python3 virtualenv dos2unix`
3. Run `dos2unix ./test/end-to-end-tests/*.sh ./test/end-to-end-tests/synapse/*.sh ./test/end-to-end-tests/riot/*.sh`
3. Run `dos2unix ./test/end-to-end-tests/*.sh ./test/end-to-end-tests/synapse/*.sh ./test/end-to-end-tests/element/*.sh`
4. Install NodeJS for ubuntu:
```bash
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get update
sudo apt-get install nodejs
```
5. Start Riot on Windows through `yarn start`
5. Start Element on Windows through `yarn start`
6. While that builds... Run:
```bash
sudo apt-get install x11-apps
@ -25,11 +25,11 @@ and start following these steps to get going:
cd ./test/end-to-end-tests
./synapse/install.sh
./install.sh
./run.sh --riot-url http://localhost:8080 --no-sandbox
./run.sh --app-url http://localhost:8080 --no-sandbox
```
Note that using `yarn test:e2e` probably won't work for you. You might also have to use the config.json from the
`riot/config-template` directory in order to actually succeed at the tests.
`element/config-template` directory in order to actually succeed at the tests.
Also note that you'll have to use `--no-sandbox` otherwise Chrome will complain that there's no sandbox available. You
could probably fix this with enough effort, or you could run a headless Chrome in the WSL container without a sandbox.

View file

@ -0,0 +1,2 @@
element-web
element.pid

View file

@ -8,7 +8,7 @@
"brand": "Element",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"bug_report_endpoint_url": "https://riot.im/bugreports/submit",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"showLabsSettings": true,
"default_federate": true,
"welcomePageUrl": "home.html",

View file

@ -0,0 +1,16 @@
#!/bin/bash
set -e
ELEMENT_BRANCH=develop
if [ -d $BASE_DIR/element-web ]; then
echo "Element is already installed"
exit
fi
curl -L https://github.com/vector-im/element-web/archive/${ELEMENT_BRANCH}.zip --output element.zip
unzip -q element.zip
rm element.zip
mv element-web-${ELEMENT_BRANCH} element-web
cd element-web
yarn install
yarn run build

View file

@ -3,7 +3,7 @@ set -e
PORT=5000
BASE_DIR=$(cd $(dirname $0) && pwd)
PIDFILE=$BASE_DIR/riot.pid
PIDFILE=$BASE_DIR/element.pid
CONFIG_BACKUP=config.e2etests_backup.json
if [ -f $PIDFILE ]; then
@ -11,8 +11,8 @@ if [ -f $PIDFILE ]; then
fi
cd $BASE_DIR/
echo -n "starting riot on http://localhost:$PORT ... "
pushd riot-web/webapp/ > /dev/null
echo -n "Starting Element on http://localhost:$PORT ... "
pushd element-web/webapp/ > /dev/null
# backup config file before we copy template
if [ -f config.json ]; then
@ -34,7 +34,7 @@ LOGFILE=$(mktemp)
# NOT expected SIGTERM (128 + 15)
# from stop.sh?
if [ $RESULT -ne 143 ]; then
echo "failed"
echo "Failed"
cat $LOGFILE
rm $PIDFILE 2> /dev/null
fi
@ -49,6 +49,6 @@ sleep 0.5 &
wait -n; RESULT=$?
# return exit code of first child to exit
if [ $RESULT -eq 0 ]; then
echo "running"
echo "Running"
fi
exit $RESULT

View file

@ -2,19 +2,19 @@
set -e
BASE_DIR=$(cd $(dirname $0) && pwd)
PIDFILE=riot.pid
PIDFILE=element.pid
CONFIG_BACKUP=config.e2etests_backup.json
cd $BASE_DIR
if [ -f $PIDFILE ]; then
echo "stopping riot server ..."
echo "Stopping Element server ..."
PID=$(cat $PIDFILE)
rm $PIDFILE
kill $PID
# revert config file
cd riot-web/webapp
cd element-web/webapp
rm config.json
if [ -f $CONFIG_BACKUP ]; then
mv $CONFIG_BACKUP config.json

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
// used from run.sh as getopts doesn't support long parameters
const idx = process.argv.indexOf("--riot-url");
let hasRiotUrl = false;
const idx = process.argv.indexOf("--app-url");
let hasAppUrl = false;
if (idx !== -1) {
const value = process.argv[idx + 1];
hasRiotUrl = !!value;
hasAppUrl = !!value;
}
process.stdout.write(hasRiotUrl ? "1" : "0" );
process.stdout.write(hasAppUrl ? "1" : "0" );

View file

@ -1,2 +0,0 @@
riot-web
riot.pid

View file

@ -1,16 +0,0 @@
#!/bin/bash
set -e
RIOT_BRANCH=develop
if [ -d $BASE_DIR/riot-web ]; then
echo "riot is already installed"
exit
fi
curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip
unzip -q riot.zip
rm riot.zip
mv riot-web-${RIOT_BRANCH} riot-web
cd riot-web
yarn install
yarn run build

View file

@ -9,16 +9,16 @@ echo "Please first run $BASE_DIR/install.sh"
exit 1
fi
has_custom_riot=$(node has_custom_riot.js $@)
has_custom_app=$(node has-custom-app.js $@)
if [ ! -d "riot/riot-web" ] && [ $has_custom_riot -ne "1" ]; then
echo "Please provide an instance of riot to test against by passing --riot-url <url> or running $BASE_DIR/riot/install.sh"
if [ ! -d "element/element-web" ] && [ $has_custom_app -ne "1" ]; then
echo "Please provide an instance of Element to test against by passing --element-url <url> or running $BASE_DIR/element/install.sh"
exit 1
fi
stop_servers() {
if [ $has_custom_riot -ne "1" ]; then
./riot/stop.sh
if [ $has_custom_app -ne "1" ]; then
./element/stop.sh
fi
./synapse/stop.sh
}
@ -32,8 +32,8 @@ handle_error() {
trap 'handle_error' ERR
./synapse/start.sh
if [ $has_custom_riot -ne "1" ]; then
./riot/start.sh
if [ $has_custom_app -ne "1" ]; then
./element/start.sh
fi
node start.js $@
stop_servers

View file

@ -22,12 +22,12 @@ const {delay} = require('./util');
const DEFAULT_TIMEOUT = 20000;
module.exports = class RiotSession {
constructor(browser, page, username, riotserver, hsUrl) {
module.exports = class ElementSession {
constructor(browser, page, username, elementServer, hsUrl) {
this.browser = browser;
this.page = page;
this.hsUrl = hsUrl;
this.riotserver = riotserver;
this.elementServer = elementServer;
this.username = username;
this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`);
this.networkLog = new LogBuffer(page, "requestfinished", async (req) => {
@ -38,7 +38,7 @@ module.exports = class RiotSession {
this.log = new Logger(this.username);
}
static async create(username, puppeteerOptions, riotserver, hsUrl, throttleCpuFactor = 1) {
static async create(username, puppeteerOptions, elementServer, hsUrl, throttleCpuFactor = 1) {
const browser = await puppeteer.launch(puppeteerOptions);
const page = await browser.newPage();
await page.setViewport({
@ -50,7 +50,7 @@ module.exports = class RiotSession {
console.log("throttling cpu by a factor of", throttleCpuFactor);
await client.send('Emulation.setCPUThrottlingRate', { rate: throttleCpuFactor });
}
return new RiotSession(browser, page, username, riotserver, hsUrl);
return new ElementSession(browser, page, username, elementServer, hsUrl);
}
async tryGetInnertext(selector) {
@ -194,7 +194,7 @@ module.exports = class RiotSession {
}
url(path) {
return this.riotserver + path;
return this.elementServer + path;
}
delay(ms) {

View file

@ -22,24 +22,12 @@ module.exports = async function signup(session, username, password, homeserver)
await session.goto(session.url('/#/register'));
// change the homeserver by clicking the advanced section
if (homeserver) {
const advancedButton = await session.query('.mx_ServerTypeSelector_type_Advanced');
await advancedButton.click();
const changeButton = await session.query('.mx_ServerPicker_change');
await changeButton.click();
// depending on what HS is configured as the default, the advanced registration
// goes the HS/IS entry directly (for matrix.org) or takes you to the user/pass entry (not matrix.org).
// To work with both, we look for the "Change" link in the user/pass entry but don't fail when we can't find it
// As this link should be visible immediately, and to not slow down the case where it isn't present,
// pick a lower timeout of 5000ms
try {
const changeHsField = await session.query('.mx_AuthBody_editServerDetails', 5000);
if (changeHsField) {
await changeHsField.click();
}
} catch (err) {}
const hsInputField = await session.query('#mx_ServerConfig_hsUrl');
const hsInputField = await session.query('.mx_ServerPickerDialog_otherHomeserver');
await session.replaceInputText(hsInputField, homeserver);
const nextButton = await session.query('.mx_Login_submit');
const nextButton = await session.query('.mx_ServerPickerDialog_continue');
// accept homeserver
await nextButton.click();
}
@ -68,7 +56,7 @@ module.exports = async function signup(session, username, password, homeserver)
await registerButton.click();
//confirm dialog saying you cant log back in without e-mail
const continueButton = await session.query('.mx_QuestionDialog button.mx_Dialog_primary');
const continueButton = await session.query('.mx_RegistrationEmailPromptDialog button.mx_Dialog_primary');
await continueButton.click();
//find the privacy policy checkbox and check it

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const RiotSession = require('./src/session');
const ElementSession = require('./src/session');
const scenario = require('./src/scenario');
const RestSessionCreator = require('./src/rest/creator');
const fs = require("fs");
@ -22,7 +22,7 @@ const fs = require("fs");
const program = require('commander');
program
.option('--no-logs', "don't output logs, document html on error", false)
.option('--riot-url [url]', "riot url to test", "http://localhost:5000")
.option('--app-url [url]', "url to test", "http://localhost:5000")
.option('--windowed', "dont run tests headless", false)
.option('--slow-mo', "type at a human speed", false)
.option('--dev-tools', "open chrome devtools in browser window", false)
@ -57,7 +57,7 @@ async function runTests() {
);
async function createSession(username) {
const session = await RiotSession.create(username, options, program.riotUrl, hsUrl, program.throttleCpu);
const session = await ElementSession.create(username, options, program.appUrl, hsUrl, program.throttleCpu);
sessions.push(session);
return session;
}

View file

@ -875,8 +875,8 @@ password_config:
# Enable sending emails for notification events
# Defining a custom URL for Riot is only needed if email notifications
# should contain links to a self-hosted installation of Riot; when set
# Defining a custom URL for Element is only needed if email notifications
# should contain links to a self-hosted installation of Element; when set
# the "app_name" setting is ignored.
#
# If your SMTP server requires authentication, the optional smtp_user &
@ -897,7 +897,7 @@ email:
notif_template_html: notif_mail.html
notif_template_text: notif_mail.txt
notif_for_new_users: True
riot_base_url: "http://localhost/riot"
client_base_url: "http://localhost/element"
#password_providers: