Merge branch 'develop' into emoji_quick_shortcut
This commit is contained in:
+3
-2
@@ -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 {
|
||||
|
||||
+63
-5
@@ -79,6 +79,8 @@ import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import Analytics from './Analytics';
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
@@ -124,7 +126,7 @@ export default class CallHandler {
|
||||
return window.mxCallHandler;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
start() {
|
||||
dis.register(this.onAction);
|
||||
// add empty handlers for media actions, otherwise the media keys
|
||||
// end up causing the audio elements with our ring/ringback etc
|
||||
@@ -137,6 +139,27 @@ export default class CallHandler {
|
||||
navigator.mediaSession.setActionHandler('previoustrack', function() {});
|
||||
navigator.mediaSession.setActionHandler('nexttrack', function() {});
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming);
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener('Call.incoming', this.onCallIncoming);
|
||||
}
|
||||
}
|
||||
|
||||
private onCallIncoming = (call) => {
|
||||
// we dispatch this synchronously to make sure that the event
|
||||
// handlers on the call are set up immediately (so that if
|
||||
// we get an immediate hangup, we don't get a stuck call)
|
||||
dis.dispatch({
|
||||
action: 'incoming_call',
|
||||
call: call,
|
||||
}, true);
|
||||
}
|
||||
|
||||
getCallForRoom(roomId: string): MatrixCall {
|
||||
@@ -204,11 +227,17 @@ export default class CallHandler {
|
||||
}
|
||||
|
||||
private setCallListeners(call: MatrixCall) {
|
||||
call.on(CallEvent.Error, (err) => {
|
||||
call.on(CallEvent.Error, (err: CallError) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
Analytics.trackEvent('voip', 'callError', 'error', err);
|
||||
Analytics.trackEvent('voip', 'callError', 'error', err.toString());
|
||||
console.error("Call error:", err);
|
||||
|
||||
if (err.code === CallErrorCode.NoUserMedia) {
|
||||
this.showMediaCaptureError(call);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
||||
@@ -277,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."),
|
||||
@@ -355,6 +385,34 @@ export default class CallHandler {
|
||||
}, null, true);
|
||||
}
|
||||
|
||||
private showMediaCaptureError(call: MatrixCall) {
|
||||
let title;
|
||||
let description;
|
||||
|
||||
if (call.type === CallType.Voice) {
|
||||
title = _t("Unable to access microphone");
|
||||
description = <div>
|
||||
{_t(
|
||||
"Call failed because no microphone could not be accessed. " +
|
||||
"Check that a microphone is plugged in and set up correctly.",
|
||||
)}
|
||||
</div>;
|
||||
} else if (call.type === CallType.Video) {
|
||||
title = _t("Unable to access webcam / microphone");
|
||||
description = <div>
|
||||
{_t("Call failed because no webcam or microphone could not be accessed. Check that:")}
|
||||
<ul>
|
||||
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
|
||||
<li>{_t("Permission is granted to use the webcam")}</li>
|
||||
<li>{_t("No other application is using the webcam")}</li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Media capture failed', '', ErrorDialog, {
|
||||
title, description,
|
||||
}, null, true);
|
||||
}
|
||||
|
||||
private placeCall(
|
||||
roomId: string, type: PlaceCallType,
|
||||
|
||||
+24
-3
@@ -27,9 +27,12 @@ import _linkifyString from 'linkifyjs/string';
|
||||
import classNames from 'classnames';
|
||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||
import url from 'url';
|
||||
import katex from 'katex';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import cheerio from 'cheerio';
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||
@@ -240,7 +243,8 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
|
||||
allowedAttributes: {
|
||||
// custom ones first:
|
||||
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||
span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
|
||||
span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
|
||||
div: ['data-mx-maths'],
|
||||
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||
img: ['src', 'width', 'height', 'alt', 'title'],
|
||||
ol: ['start'],
|
||||
@@ -414,6 +418,21 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
||||
if (isHtmlMessage) {
|
||||
isDisplayedWithHtml = true;
|
||||
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
|
||||
|
||||
if (SettingsStore.getValue("feature_latex_maths")) {
|
||||
const phtml = cheerio.load(safeBody,
|
||||
{ _useHtmlParser2: true, decodeEntities: false })
|
||||
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
|
||||
return katex.renderToString(
|
||||
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
|
||||
{
|
||||
throwOnError: false,
|
||||
displayMode: e.name == 'div',
|
||||
output: "htmlAndMathml",
|
||||
});
|
||||
});
|
||||
safeBody = phtml.html();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
delete sanitizeParams.textFilter;
|
||||
@@ -515,7 +534,6 @@ export function checkBlockNode(node: Node) {
|
||||
case "H6":
|
||||
case "PRE":
|
||||
case "BLOCKQUOTE":
|
||||
case "DIV":
|
||||
case "P":
|
||||
case "UL":
|
||||
case "OL":
|
||||
@@ -528,6 +546,9 @@ export function checkBlockNode(node: Node) {
|
||||
case "TH":
|
||||
case "TD":
|
||||
return true;
|
||||
case "DIV":
|
||||
// don't treat math nodes as block nodes for deserializing
|
||||
return !(node as HTMLElement).hasAttribute("data-mx-maths");
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
+8
-3
@@ -48,6 +48,8 @@ import {Jitsi} from "./widgets/Jitsi";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import CallHandler from './CallHandler';
|
||||
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
@@ -588,9 +590,9 @@ export function logout(): void {
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
// logout doesn't work for guest sessions
|
||||
// Also we sometimes want to re-log in a guest session
|
||||
// if we abort the login
|
||||
onLoggedOut();
|
||||
// Also we sometimes want to re-log in a guest session if we abort the login.
|
||||
// defer until next tick because it calls a synchronous dispatch and we are likely here from a dispatch.
|
||||
setImmediate(() => onLoggedOut());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -665,6 +667,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
||||
DMRoomMap.makeShared().start();
|
||||
IntegrationManagers.sharedInstance().startWatching();
|
||||
ActiveWidgetStore.start();
|
||||
CallHandler.sharedInstance().start();
|
||||
|
||||
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
|
||||
// the thing just wastes CPU cycles, but should result in no actual functionality
|
||||
@@ -714,6 +717,7 @@ export async function onLoggedOut(): Promise<void> {
|
||||
dis.dispatch({action: 'on_logged_out'}, true);
|
||||
stopMatrixClient();
|
||||
await clearStorage({deleteEverything: true});
|
||||
LifecycleCustomisations.onLoggedOutAndStorageCleared?.();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -760,6 +764,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
|
||||
*/
|
||||
export function stopMatrixClient(unsetClient = true): void {
|
||||
Notifier.stop();
|
||||
CallHandler.sharedInstance().stop();
|
||||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
||||
Presence.stop();
|
||||
|
||||
+19
-20
@@ -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,
|
||||
|
||||
@@ -23,6 +23,11 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||
|
||||
function is_allowed_html_tag(node) {
|
||||
if (node.literal != null &&
|
||||
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Regex won't work for tags with attrs, but we only
|
||||
// allow <del> anyway.
|
||||
const matches = /^<\/?(.*)>$/.exec(node.literal);
|
||||
@@ -30,6 +35,7 @@ function is_allowed_html_tag(node) {
|
||||
const tag = matches[1];
|
||||
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,15 @@ export class ModalManager {
|
||||
return this.appendDialogAsync<T>(...rest);
|
||||
}
|
||||
|
||||
public closeCurrentModal(reason: string) {
|
||||
const modal = this.getCurrentModal();
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
modal.closeReason = reason;
|
||||
modal.close();
|
||||
}
|
||||
|
||||
private buildModal<T extends any[]>(
|
||||
prom: Promise<React.ComponentType>,
|
||||
props?: IProps<T>,
|
||||
|
||||
@@ -34,6 +34,8 @@ import SettingsStore from "./settings/SettingsStore";
|
||||
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
|
||||
import {SettingLevel} from "./settings/SettingLevel";
|
||||
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import UserActivity from "./UserActivity";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
@@ -376,6 +378,11 @@ export const Notifier = {
|
||||
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||
if (actions && actions.notify) {
|
||||
if (RoomViewStore.getRoomId() === room.roomId && UserActivity.sharedInstance().userActiveRecently()) {
|
||||
// don't bother notifying as user was recently active in this room
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEnabled()) {
|
||||
this._displayPopupNotification(ev, room);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
+2
-2
@@ -40,11 +40,11 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
||||
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
||||
}
|
||||
|
||||
export function showStartChatInviteDialog() {
|
||||
export function showStartChatInviteDialog(initialText) {
|
||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
||||
'Start DM', '', InviteDialog, {kind: KIND_DM, initialText},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
function tsOfNewestEvent(room) {
|
||||
if (room.timeline.length) {
|
||||
return room.timeline[room.timeline.length - 1].getTs();
|
||||
} else {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
}
|
||||
|
||||
export function mostRecentActivityFirst(roomList) {
|
||||
return roomList.sort(function(a, b) {
|
||||
return tsOfNewestEvent(b) - tsOfNewestEvent(a);
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -455,7 +455,7 @@ function textForWidgetEvent(event) {
|
||||
let widgetName = name || prevName || type || prevType || '';
|
||||
// Apply sentence case to widget name
|
||||
if (widgetName && widgetName.length > 0) {
|
||||
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1) + ' ';
|
||||
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1);
|
||||
}
|
||||
|
||||
// If the widget was removed, its content should be {}, but this is sufficiently
|
||||
|
||||
@@ -257,6 +257,12 @@ const shortcuts: Record<Categories, IShortcut[]> = {
|
||||
key: Key.SLASH,
|
||||
}],
|
||||
description: _td("Toggle this dialog"),
|
||||
}, {
|
||||
keybinds: [{
|
||||
modifiers: [CMD_OR_CTRL, Modifiers.ALT],
|
||||
key: Key.H,
|
||||
}],
|
||||
description: _td("Go to Home View"),
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const LONG_DESC_PLACEHOLDER = _td(
|
||||
some important <a href="foo">links</a>
|
||||
</p>
|
||||
<p>
|
||||
You can even use 'img' tags
|
||||
You can even add images with Matrix URLs <img src="mxc://url" />
|
||||
</p>
|
||||
`);
|
||||
|
||||
|
||||
@@ -31,10 +31,26 @@ import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import MiniAvatarUploader, {AVATAR_SIZE} from "../views/elements/MiniAvatarUploader";
|
||||
import Analytics from "../../Analytics";
|
||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||
|
||||
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
|
||||
const onClickExplore = () => dis.fire(Action.ViewRoomDirectory);
|
||||
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'});
|
||||
const onClickSendDm = () => {
|
||||
Analytics.trackEvent('home_page', 'button', 'dm');
|
||||
CountlyAnalytics.instance.track("home_page_button", { button: "dm" });
|
||||
dis.dispatch({action: 'view_create_chat'});
|
||||
};
|
||||
|
||||
const onClickExplore = () => {
|
||||
Analytics.trackEvent('home_page', 'button', 'room_directory');
|
||||
CountlyAnalytics.instance.track("home_page_button", { button: "room_directory" });
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
|
||||
const onClickNewRoom = () => {
|
||||
Analytics.trackEvent('home_page', 'button', 'create_room');
|
||||
CountlyAnalytics.instance.track("home_page_button", { button: "create_room" });
|
||||
dis.dispatch({action: 'view_create_room'});
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
justRegistered?: boolean;
|
||||
|
||||
@@ -21,7 +21,7 @@ import * as PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isMac} from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
@@ -52,6 +52,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import Modal from "../../Modal";
|
||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
@@ -392,6 +393,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
|
||||
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
const modKey = isMac ? ev.metaKey : ev.ctrlKey;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.PAGE_UP:
|
||||
@@ -436,6 +438,16 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.H:
|
||||
if (ev.altKey && modKey) {
|
||||
dis.dispatch({
|
||||
action: 'view_home_page',
|
||||
});
|
||||
Modal.closeCurrentModal("homeKeyboardShortcut");
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_UP:
|
||||
case Key.ARROW_DOWN:
|
||||
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
|
||||
@@ -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';
|
||||
@@ -87,38 +85,37 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
export enum Views {
|
||||
// a special initial state which is only used at startup, while we are
|
||||
// trying to re-animate a matrix client or register as a guest.
|
||||
LOADING = 0,
|
||||
LOADING,
|
||||
|
||||
// we are showing the welcome view
|
||||
WELCOME = 1,
|
||||
WELCOME,
|
||||
|
||||
// we are showing the login view
|
||||
LOGIN = 2,
|
||||
LOGIN,
|
||||
|
||||
// we are showing the registration view
|
||||
REGISTER = 3,
|
||||
|
||||
// completing the registration flow
|
||||
POST_REGISTRATION = 4,
|
||||
REGISTER,
|
||||
|
||||
// showing the 'forgot password' view
|
||||
FORGOT_PASSWORD = 5,
|
||||
FORGOT_PASSWORD,
|
||||
|
||||
// showing flow to trust this new device with cross-signing
|
||||
COMPLETE_SECURITY = 6,
|
||||
COMPLETE_SECURITY,
|
||||
|
||||
// flow to setup SSSS / cross-signing on this account
|
||||
E2E_SETUP = 7,
|
||||
E2E_SETUP,
|
||||
|
||||
// we are logged in with an active matrix client. The logged_in state also
|
||||
// includes guests users as they too are logged in at the client level.
|
||||
LOGGED_IN = 8,
|
||||
LOGGED_IN,
|
||||
|
||||
// We are logged out (invalid token) but have our local state again. The user
|
||||
// should log back in to rehydrate the client.
|
||||
SOFT_LOGOUT = 9,
|
||||
SOFT_LOGOUT,
|
||||
}
|
||||
|
||||
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas"];
|
||||
|
||||
// Actions that are redirected through the onboarding process prior to being
|
||||
// re-dispatched. NOTE: some actions are non-trivial and would require
|
||||
// re-factoring to be included in this list in future.
|
||||
@@ -562,11 +559,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
ThemeController.isLogin = true;
|
||||
this.themeWatcher.recheck();
|
||||
break;
|
||||
case 'start_post_registration':
|
||||
this.setState({
|
||||
view: Views.POST_REGISTRATION,
|
||||
});
|
||||
break;
|
||||
case 'start_password_recovery':
|
||||
this.setStateForNewView({
|
||||
view: Views.FORGOT_PASSWORD,
|
||||
@@ -597,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();
|
||||
@@ -626,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");
|
||||
@@ -653,8 +642,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
case Action.ViewRoomDirectory: {
|
||||
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {},
|
||||
'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
initialText: payload.initialText,
|
||||
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this.viewSomethingBehindModal();
|
||||
@@ -677,7 +667,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.chatCreateOrReuse(payload.user_id);
|
||||
break;
|
||||
case 'view_create_chat':
|
||||
showStartChatInviteDialog();
|
||||
showStartChatInviteDialog(payload.initialText || "");
|
||||
break;
|
||||
case 'view_invite':
|
||||
showRoomInviteDialog(payload.roomId);
|
||||
@@ -807,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
|
||||
@@ -1102,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");
|
||||
@@ -1238,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'});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1358,18 +1315,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
});
|
||||
});
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
cli.on('Call.incoming', function(call) {
|
||||
// we dispatch this synchronously to make sure that the event
|
||||
// handlers on the call are set up immediately (so that if
|
||||
// we get an immediate hangup, we don't get a stuck call)
|
||||
dis.dispatch({
|
||||
action: 'incoming_call',
|
||||
call: call,
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
cli.on('Session.logged_out', function(errObj) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
|
||||
@@ -1554,6 +1499,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
|
||||
showScreen(screen: string, params?: {[key: string]: any}) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const isLoggedOutOrGuest = !cli || cli.isGuest();
|
||||
if (!isLoggedOutOrGuest && AUTH_SCREENS.includes(screen)) {
|
||||
// user is logged in and landing on an auth page which will uproot their session, redirect them home instead
|
||||
dis.dispatch({ action: "view_home_page" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen === 'register') {
|
||||
dis.dispatch({
|
||||
action: 'start_registration',
|
||||
@@ -1570,7 +1523,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
params: params,
|
||||
});
|
||||
} else if (screen === 'soft_logout') {
|
||||
if (MatrixClientPeg.get() && MatrixClientPeg.get().getUserId() && !Lifecycle.isSoftLogout()) {
|
||||
if (cli.getUserId() && !Lifecycle.isSoftLogout()) {
|
||||
// Logged in - visit a room
|
||||
this.viewLastRoom();
|
||||
} else {
|
||||
@@ -1621,14 +1574,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
dis.dispatch({
|
||||
action: 'view_my_groups',
|
||||
});
|
||||
} else if (screen === 'complete_security') {
|
||||
dis.dispatch({
|
||||
action: 'start_complete_security',
|
||||
});
|
||||
} else if (screen === 'post_registration') {
|
||||
dis.dispatch({
|
||||
action: 'start_post_registration',
|
||||
});
|
||||
} else if (screen.indexOf('room/') === 0) {
|
||||
// Rooms can have the following formats:
|
||||
// #room_alias:domain or !opaque_id:domain
|
||||
@@ -1799,14 +1744,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
return Lifecycle.setLoggedIn(credentials);
|
||||
}
|
||||
|
||||
onFinishPostRegistration = () => {
|
||||
// Don't confuse this with "PageType" which is the middle window to show
|
||||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
});
|
||||
this.showScreen("settings");
|
||||
};
|
||||
|
||||
onSendEvent(roomId: string, event: MatrixEvent) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
@@ -1971,13 +1908,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
accountPassword={this.accountPassword}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.view === Views.POST_REGISTRATION) {
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
const PostRegistration = sdk.getComponent('structures.auth.PostRegistration');
|
||||
view = (
|
||||
<PostRegistration
|
||||
onComplete={this.onFinishPostRegistration} />
|
||||
);
|
||||
} else if (this.state.view === Views.LOGGED_IN) {
|
||||
// store errors stop the client syncing and require user intervention, so we'll
|
||||
// be showing a dialog. Don't show anything else.
|
||||
@@ -2041,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()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -44,6 +44,7 @@ function track(action) {
|
||||
|
||||
export default class RoomDirectory extends React.Component {
|
||||
static propTypes = {
|
||||
initialText: PropTypes.string,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -61,7 +62,7 @@ export default class RoomDirectory extends React.Component {
|
||||
error: null,
|
||||
instanceId: undefined,
|
||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||
filterString: null,
|
||||
filterString: this.props.initialText || "",
|
||||
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
|
||||
? selectedCommunityId
|
||||
: null,
|
||||
@@ -686,6 +687,7 @@ export default class RoomDirectory extends React.Component {
|
||||
onJoinClick={this.onJoinFromSearchClick}
|
||||
placeholder={placeholder}
|
||||
showJoinButton={showJoinButton}
|
||||
initialText={this.props.initialText}
|
||||
/>
|
||||
{dropdown}
|
||||
</div>;
|
||||
|
||||
@@ -148,7 +148,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={_t("Search")}
|
||||
placeholder={_t("Filter")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
@@ -164,7 +164,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
if (this.props.isMinimized) {
|
||||
icon = (
|
||||
<AccessibleButton
|
||||
title={_t("Search rooms")}
|
||||
title={_t("Filter rooms and people")}
|
||||
className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle"
|
||||
onClick={this.openSearch}
|
||||
/>
|
||||
|
||||
@@ -18,13 +18,11 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import * as sdk from '../../index';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import Resend from '../../Resend';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import { CallState, CallType } from 'matrix-js-sdk/lib/webrtc/call';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
@@ -42,13 +40,6 @@ export default class RoomStatusBar extends React.Component {
|
||||
// the room this statusbar is representing.
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// The active call in the room, if any (means we show the call bar
|
||||
// along with the status of the call)
|
||||
callState: PropTypes.string,
|
||||
|
||||
// The type of the call in progress, or null if no call is in progress
|
||||
callType: PropTypes.string,
|
||||
|
||||
// true if the room is being peeked at. This affects components that shouldn't
|
||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||
isPeeking: PropTypes.bool,
|
||||
@@ -115,12 +106,6 @@ export default class RoomStatusBar extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
_showCallBar() {
|
||||
return (this.props.callState &&
|
||||
(this.props.callState !== CallState.Ended && this.props.callState !== CallState.Ringing)
|
||||
);
|
||||
}
|
||||
|
||||
_onResendAllClick = () => {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
dis.fire(Action.FocusComposer);
|
||||
@@ -152,7 +137,7 @@ export default class RoomStatusBar extends React.Component {
|
||||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
_getSize() {
|
||||
if (this._shouldShowConnectionError() || this._showCallBar()) {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (this.state.unsentMessages.length > 0) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
@@ -160,22 +145,6 @@ export default class RoomStatusBar extends React.Component {
|
||||
return STATUS_BAR_HIDDEN;
|
||||
}
|
||||
|
||||
// return suitable content for the image on the left of the status bar.
|
||||
_getIndicator() {
|
||||
if (this._showCallBar()) {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
return (
|
||||
<TintableSvg src={require("../../../res/img/element-icons/room/in-call.svg")} width="23" height="20" />
|
||||
);
|
||||
}
|
||||
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_shouldShowConnectionError() {
|
||||
// no conn bar trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
@@ -266,25 +235,6 @@ export default class RoomStatusBar extends React.Component {
|
||||
</div>;
|
||||
}
|
||||
|
||||
_getCallStatusText() {
|
||||
switch (this.props.callState) {
|
||||
case CallState.CreateOffer:
|
||||
case CallState.InviteSent:
|
||||
return _t('Calling...');
|
||||
case CallState.Connecting:
|
||||
case CallState.CreateAnswer:
|
||||
return _t('Call connecting...');
|
||||
case CallState.Connected:
|
||||
return _t('Active call');
|
||||
case CallState.WaitLocalMedia:
|
||||
if (this.props.callType === CallType.Video) {
|
||||
return _t('Starting camera...');
|
||||
} else {
|
||||
return _t('Starting microphone...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return suitable content for the main (text) part of the status bar.
|
||||
_getContent() {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
@@ -307,26 +257,14 @@ export default class RoomStatusBar extends React.Component {
|
||||
return this._getUnsentMessageContent();
|
||||
}
|
||||
|
||||
if (this._showCallBar()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_callBar">
|
||||
<b>{ this._getCallStatusText() }</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this._getContent();
|
||||
const indicator = this._getIndicator();
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar">
|
||||
<div className="mx_RoomStatusBar_indicator">
|
||||
{ indicator }
|
||||
</div>
|
||||
<div role="alert">
|
||||
{ content }
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ import rateLimitedFunc from '../../ratelimitedfunc';
|
||||
import * as ObjectUtils from '../../ObjectUtils';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, {searchPagination} from '../../Searching';
|
||||
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
|
||||
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard';
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
@@ -56,7 +56,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import {SettingLevel} from "../../settings/SettingLevel";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
import {IMatrixClientCreds} from "../../MatrixClientPeg";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
@@ -68,10 +67,9 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import TintableSvg from "../views/elements/TintableSvg";
|
||||
import {XOR} from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
@@ -508,8 +506,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
}
|
||||
this.onResize();
|
||||
|
||||
document.addEventListener("keydown", this.onNativeKeyDown);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
@@ -592,8 +588,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
|
||||
}
|
||||
|
||||
document.removeEventListener("keydown", this.onNativeKeyDown);
|
||||
|
||||
// Remove RoomStore listener
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
@@ -642,33 +636,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||
private onNativeKeyDown = ev => {
|
||||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.D:
|
||||
if (ctrlCmdOnly) {
|
||||
this.onMuteAudioClick();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.E:
|
||||
if (ctrlCmdOnly) {
|
||||
this.onMuteVideoClick();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onReactKeyDown = ev => {
|
||||
let handled = false;
|
||||
|
||||
@@ -1324,10 +1291,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onSettingsClick = () => {
|
||||
dis.dispatch({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.RoomSummary,
|
||||
});
|
||||
dis.dispatch({ action: "open_room_settings" });
|
||||
};
|
||||
|
||||
private onCancelClick = () => {
|
||||
@@ -1368,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,
|
||||
});
|
||||
@@ -1402,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,
|
||||
});
|
||||
@@ -1758,8 +1722,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||
statusBar = <RoomStatusBar
|
||||
room={this.state.room}
|
||||
callState={this.state.callState}
|
||||
callType={activeCall ? activeCall.type : null}
|
||||
isPeeking={myMembership !== "join"}
|
||||
onInviteClick={this.onInviteButtonClick}
|
||||
onVisible={this.onStatusBarVisible}
|
||||
@@ -1883,56 +1845,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
};
|
||||
}
|
||||
|
||||
if (activeCall) {
|
||||
let zoomButton; let videoMuteButton;
|
||||
|
||||
if (activeCall.type === CallType.Video) {
|
||||
zoomButton = (
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={_t("Fill screen")}>
|
||||
<TintableSvg
|
||||
src={require("../../../res/img/element-icons/call/fullscreen.svg")}
|
||||
width="29"
|
||||
height="22"
|
||||
style={{ marginTop: 1, marginRight: 4 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
videoMuteButton =
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
|
||||
<TintableSvg
|
||||
src={activeCall.isLocalVideoMuted() ?
|
||||
require("../../../res/img/element-icons/call/video-muted.svg") :
|
||||
require("../../../res/img/element-icons/call/video-call.svg")}
|
||||
alt={activeCall.isLocalVideoMuted() ? _t("Click to unmute video") :
|
||||
_t("Click to mute video")}
|
||||
width=""
|
||||
height="27"
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
const voiceMuteButton =
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
||||
<TintableSvg
|
||||
src={activeCall.isMicrophoneMuted() ?
|
||||
require("../../../res/img/element-icons/call/voice-muted.svg") :
|
||||
require("../../../res/img/element-icons/call/voice-unmuted.svg")}
|
||||
alt={activeCall.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
|
||||
width="21"
|
||||
height="26"
|
||||
/>
|
||||
</div>;
|
||||
|
||||
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
|
||||
statusBar =
|
||||
<div className="mx_RoomView_callStatusBar">
|
||||
{ voiceMuteButton }
|
||||
{ videoMuteButton }
|
||||
{ zoomButton }
|
||||
{ statusBar }
|
||||
</div>;
|
||||
}
|
||||
|
||||
// if we have search results, we keep the messagepanel (so that it preserves its
|
||||
// scroll state), but hide it.
|
||||
let searchResultsPanel;
|
||||
|
||||
@@ -55,11 +55,11 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||
let toast;
|
||||
if (totalCount !== 0) {
|
||||
const topToast = this.state.toasts[0];
|
||||
const {title, icon, key, component, props} = topToast;
|
||||
const {title, icon, key, component, className, props} = topToast;
|
||||
const toastClasses = classNames("mx_Toast_toast", {
|
||||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
});
|
||||
}, className);
|
||||
|
||||
let countIndicator;
|
||||
if (isStacked || this.state.countSeen > 0) {
|
||||
|
||||
@@ -29,7 +29,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
@@ -205,6 +205,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onSignInClick = () => {
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onRegisterClick = () => {
|
||||
dis.dispatch({ action: 'start_registration' });
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -261,10 +271,29 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let hostingLink;
|
||||
let topSection;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
if (signupLink) {
|
||||
hostingLink = (
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||
{_t("Got an account? <a>Sign in</a>", {}, {
|
||||
a: sub => (
|
||||
<AccessibleButton kind="link" onClick={this.onSignInClick}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
{_t("New here? <a>Create an account</a>", {}, {
|
||||
a: sub => (
|
||||
<AccessibleButton kind="link" onClick={this.onRegisterClick}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
} else if (signupLink) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
@@ -422,6 +451,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
)
|
||||
} else if (MatrixClientPeg.get().isGuest()) {
|
||||
primaryOptionList = (
|
||||
<React.Fragment>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ homeButton }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
@@ -451,7 +494,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
/>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
{hostingLink}
|
||||
{topSection}
|
||||
{primaryOptionList}
|
||||
{secondarySection}
|
||||
</IconizedContextMenu>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+179
-282
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 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.
|
||||
@@ -16,33 +14,27 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
const PHASE_SERVER_DETAILS = 0;
|
||||
// Show the appropriate login flow(s) for the server
|
||||
const PHASE_LOGIN = 1;
|
||||
|
||||
// Enable phases for login
|
||||
const PHASES_ENABLED = true;
|
||||
import {IMatrixClientCreds} from "../../../MatrixClientPeg";
|
||||
import PasswordLogin from "../../views/auth/PasswordLogin";
|
||||
import InlineSpinner from "../../views/elements/InlineSpinner";
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
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.
|
||||
@@ -55,64 +47,80 @@ _td("Invalid base_url for m.identity_server");
|
||||
_td("Identity server URL does not appear to be a valid identity server");
|
||||
_td("General failure");
|
||||
|
||||
interface IProps {
|
||||
serverConfig: ValidatedServerConfig;
|
||||
// If true, the component will consider itself busy.
|
||||
busy?: boolean;
|
||||
isSyncing?: boolean;
|
||||
// Secondary HS which we try to log into if the user is using
|
||||
// the default HS but login fails. Useful for migrating to a
|
||||
// different homeserver without confusing users.
|
||||
fallbackHsUrl?: string;
|
||||
defaultDeviceDisplayName?: string;
|
||||
fragmentAfterLogin?: string;
|
||||
|
||||
// Called when the user has logged in. Params:
|
||||
// - The object returned by the login API
|
||||
// - The user's password, if applicable, (may be cached in memory for a
|
||||
// short time so the user is not required to re-enter their password
|
||||
// for operations like uploading cross-signing keys).
|
||||
onLoggedIn(data: IMatrixClientCreds, password: string): void;
|
||||
|
||||
// login shouldn't know or care how registration, password recovery, etc is done.
|
||||
onRegisterClick(): void;
|
||||
onForgotPasswordClick?(): void;
|
||||
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
busyLoggingIn?: boolean;
|
||||
errorText?: ReactNode;
|
||||
loginIncorrect: boolean;
|
||||
// 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;
|
||||
|
||||
// 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
|
||||
// be seeing.
|
||||
serverIsAlive: boolean;
|
||||
serverErrorIsFatal: boolean;
|
||||
serverDeadError: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
*/
|
||||
export default class LoginComponent extends React.Component {
|
||||
static propTypes = {
|
||||
// Called when the user has logged in. Params:
|
||||
// - The object returned by the login API
|
||||
// - The user's password, if applicable, (may be cached in memory for a
|
||||
// short time so the user is not required to re-enter their password
|
||||
// for operations like uploading cross-signing keys).
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
export default class LoginComponent extends React.PureComponent<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private loginLogic: Login;
|
||||
|
||||
// If true, the component will consider itself busy.
|
||||
busy: PropTypes.bool,
|
||||
|
||||
// Secondary HS which we try to log into if the user is using
|
||||
// the default HS but login fails. Useful for migrating to a
|
||||
// different homeserver without confusing users.
|
||||
fallbackHsUrl: PropTypes.string,
|
||||
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// login shouldn't know or care how registration, password recovery,
|
||||
// etc is done.
|
||||
onRegisterClick: PropTypes.func.isRequired,
|
||||
onForgotPasswordClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
isSyncing: PropTypes.bool,
|
||||
};
|
||||
private readonly stepRendererMap: Record<string, () => ReactNode>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this.state = {
|
||||
busy: false,
|
||||
busyLoggingIn: null,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
canTryLogin: true, // can we attempt to log in or are there validation errors?
|
||||
canTryLogin: true,
|
||||
|
||||
flows: null,
|
||||
|
||||
// used for preserving form values when changing homeserver
|
||||
username: "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
|
||||
// Phase of the overall login dialog.
|
||||
phase: PHASE_LOGIN,
|
||||
// The current login flow, such as password, SSO, etc.
|
||||
currentFlow: null, // we need to load the flows from the server
|
||||
|
||||
// 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
|
||||
// be seeing.
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
@@ -120,12 +128,12 @@ export default class LoginComponent extends React.Component {
|
||||
|
||||
// map from login step type to a function which will render a control
|
||||
// letting you do that login type
|
||||
this._stepRendererMap = {
|
||||
'm.login.password': this._renderPasswordStep,
|
||||
this.stepRendererMap = {
|
||||
'm.login.password': this.renderPasswordStep,
|
||||
|
||||
// CAS and SSO are the same thing, modulo the url we link to
|
||||
'm.login.cas': () => this._renderSsoStep("cas"),
|
||||
'm.login.sso': () => this._renderSsoStep("sso"),
|
||||
'm.login.cas': () => this.renderSsoStep("cas"),
|
||||
'm.login.sso': () => this.renderSsoStep("sso"),
|
||||
};
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_login_begin");
|
||||
@@ -134,11 +142,11 @@ export default class LoginComponent extends React.Component {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this._initLoginLogic();
|
||||
this.initLoginLogic(this.props.serverConfig);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
@@ -148,16 +156,9 @@ export default class LoginComponent extends React.Component {
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
// Ensure that we end up actually logging in to the right place
|
||||
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
||||
this.initLoginLogic(newProps.serverConfig);
|
||||
}
|
||||
|
||||
onPasswordLoginError = errorText => {
|
||||
this.setState({
|
||||
errorText,
|
||||
loginIncorrect: Boolean(errorText),
|
||||
});
|
||||
};
|
||||
|
||||
isBusy = () => this.state.busy || this.props.busy;
|
||||
|
||||
onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => {
|
||||
@@ -194,13 +195,13 @@ export default class LoginComponent extends React.Component {
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
this._loginLogic.loginViaPassword(
|
||||
this.loginLogic.loginViaPassword(
|
||||
username, phoneCountry, phoneNumber, password,
|
||||
).then((data) => {
|
||||
this.setState({serverIsAlive: true}); // it must be, we logged in.
|
||||
this.props.onLoggedIn(data, password);
|
||||
}, (error) => {
|
||||
if (this._unmounted) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
let errorText;
|
||||
@@ -212,21 +213,23 @@ export default class LoginComponent extends React.Component {
|
||||
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
error.data.admin_contact,
|
||||
{
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
},
|
||||
);
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
error.data.admin_contact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to continue using this service."),
|
||||
},
|
||||
);
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{errorTop}</div>
|
||||
@@ -253,7 +256,7 @@ export default class LoginComponent extends React.Component {
|
||||
}
|
||||
} else {
|
||||
// other errors, not specific to doing a password login
|
||||
errorText = this._errorTextFromError(error);
|
||||
errorText = this.errorTextFromError(error);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -291,7 +294,7 @@ export default class LoginComponent extends React.Component {
|
||||
// the busy state. In the case of a full MXID that resolves to the same
|
||||
// HS as Element's default HS though, there may not be any server change.
|
||||
// To avoid this trap, we clear busy here. For cases where the server
|
||||
// actually has changed, `_initLoginLogic` will be called and manages
|
||||
// actually has changed, `initLoginLogic` will be called and manages
|
||||
// busy state for its own liveness check.
|
||||
this.setState({
|
||||
busy: false,
|
||||
@@ -304,7 +307,7 @@ export default class LoginComponent extends React.Component {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
|
||||
let errorText = message;
|
||||
let errorText: ReactNode = message;
|
||||
let discoveryState = {};
|
||||
if (AutoDiscoveryUtils.isLivelinessError(e)) {
|
||||
errorText = this.state.errorText;
|
||||
@@ -330,21 +333,6 @@ export default class LoginComponent extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
onPhoneNumberBlur = phoneNumber => {
|
||||
// Validate the phone number entered
|
||||
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
|
||||
this.setState({
|
||||
errorText: _t('The phone number entered looks invalid'),
|
||||
canTryLogin: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
canTryLogin: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRegisterClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -352,14 +340,16 @@ export default class LoginComponent extends React.Component {
|
||||
};
|
||||
|
||||
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';
|
||||
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind,
|
||||
const ssoKind = ssoFlow.type === 'm.login.sso' ? 'sso' : 'cas';
|
||||
PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind,
|
||||
this.props.fragmentAfterLogin);
|
||||
} else {
|
||||
// Don't intercept - just go through to the register page
|
||||
@@ -367,24 +357,7 @@ export default class LoginComponent extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
onServerDetailsNextPhaseClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
});
|
||||
};
|
||||
|
||||
onEditServerDetailsClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
};
|
||||
|
||||
async _initLoginLogic(hsUrl, isUrl) {
|
||||
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
|
||||
isUrl = isUrl || this.props.serverConfig.isUrl;
|
||||
|
||||
private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
|
||||
let isDefaultServer = false;
|
||||
if (this.props.serverConfig.isDefault
|
||||
&& hsUrl === this.props.serverConfig.hsUrl
|
||||
@@ -397,11 +370,10 @@ export default class LoginComponent extends React.Component {
|
||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
});
|
||||
this._loginLogic = loginLogic;
|
||||
this.loginLogic = loginLogic;
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
currentFlow: null, // reset flow
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
@@ -425,42 +397,26 @@ export default class LoginComponent extends React.Component {
|
||||
busy: false,
|
||||
...AutoDiscoveryUtils.authComponentStateForError(e),
|
||||
});
|
||||
if (this.state.serverErrorIsFatal) {
|
||||
// Server is dead: show server details prompt instead
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
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({
|
||||
errorText: this._errorTextFromError(err),
|
||||
errorText: this.errorTextFromError(err),
|
||||
loginIncorrect: false,
|
||||
canTryLogin: false,
|
||||
});
|
||||
@@ -471,28 +427,24 @@ export default class LoginComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
_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]) {
|
||||
if (!this.stepRendererMap[flow.type]) {
|
||||
console.log("Skipping flow", flow, "due to unsupported login type", flow.type);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
_getCurrentFlowStep() {
|
||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
||||
}
|
||||
|
||||
_errorTextFromError(err) {
|
||||
private errorTextFromError(err: MatrixError): ReactNode {
|
||||
let errCode = err.errcode;
|
||||
if (!errCode && err.httpStatus) {
|
||||
errCode = "HTTP " + err.httpStatus;
|
||||
}
|
||||
|
||||
let errorText = _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:' &&
|
||||
@@ -502,29 +454,27 @@ export default class LoginComponent extends React.Component {
|
||||
errorText = <span>
|
||||
{ _t("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>.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener"
|
||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||
>
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener"
|
||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||
>
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
) }
|
||||
}) }
|
||||
</span>;
|
||||
} else {
|
||||
errorText = <span>
|
||||
{ _t("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.", {},
|
||||
{
|
||||
'a': (sub) =>
|
||||
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
{
|
||||
'a': (sub) =>
|
||||
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
||||
{ sub }
|
||||
</a>,
|
||||
}) }
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
@@ -532,121 +482,63 @@ export default class LoginComponent extends React.Component {
|
||||
return errorText;
|
||||
}
|
||||
|
||||
renderServerComponent() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
renderLoginComponentForFlows() {
|
||||
if (!this.state.flows) return null;
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
// this is the ideal order we want to show the flows in
|
||||
const order = [
|
||||
"m.login.password",
|
||||
"m.login.sso",
|
||||
];
|
||||
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serverDetailsProps = {};
|
||||
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}
|
||||
/>;
|
||||
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>
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_renderPasswordStep = () => {
|
||||
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private renderPasswordStep = () => {
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onError={this.onPasswordLoginError}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onUsernameBlur={this.onUsernameBlur}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
serverConfig={this.props.serverConfig}
|
||||
disableSubmit={this.isBusy()}
|
||||
busy={this.props.isSyncing || this.state.busyLoggingIn}
|
||||
onSubmit={this.onPasswordLogin}
|
||||
username={this.state.username}
|
||||
phoneCountry={this.state.phoneCountry}
|
||||
phoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onUsernameBlur={this.onUsernameBlur}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
serverConfig={this.props.serverConfig}
|
||||
disableSubmit={this.isBusy()}
|
||||
busy={this.props.isSyncing || this.state.busyLoggingIn}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderSsoStep = loginType => {
|
||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
||||
private renderSsoStep = loginType => {
|
||||
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
|
||||
|
||||
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} />
|
||||
|
||||
<SSOButton
|
||||
className="mx_Login_sso_link mx_Login_submit"
|
||||
matrixClient={this._loginLogic.createTemporaryClient()}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
/>
|
||||
</div>
|
||||
<SSOButtons
|
||||
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||
flow={flow}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const loader = this.isBusy() && !this.state.busyLoggingIn ?
|
||||
<div className="mx_Login_loader"><Loader /></div> : null;
|
||||
<div className="mx_Login_loader"><Spinner /></div> : null;
|
||||
|
||||
const errorText = this.state.errorText;
|
||||
|
||||
@@ -686,9 +578,11 @@ export default class LoginComponent extends React.Component {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -702,8 +596,11 @@ export default class LoginComponent extends React.Component {
|
||||
</h2>
|
||||
{ errorTextSection }
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderLoginComponentForStep() }
|
||||
<ServerPicker
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
/>
|
||||
{ this.renderLoginComponentForFlows() }
|
||||
{ footer }
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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 * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
|
||||
export default class PostRegistration extends React.Component {
|
||||
static propTypes = {
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
avatarUrl: null,
|
||||
errorString: null,
|
||||
busy: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
|
||||
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
|
||||
// the URL to be passed to you (because it's also used for room avatars).
|
||||
const cli = MatrixClientPeg.get();
|
||||
this.setState({busy: true});
|
||||
const self = this;
|
||||
cli.getProfileInfo(cli.credentials.userId).then(function(result) {
|
||||
self.setState({
|
||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
|
||||
busy: false,
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
errorString: _t("Failed to fetch avatar URL"),
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
const AuthHeader = sdk.getComponent('auth.AuthHeader');
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<div className="mx_Login_profile">
|
||||
{ _t('Set a display name:') }
|
||||
<ChangeDisplayName />
|
||||
{ _t('Upload an avatar:') }
|
||||
<ChangeAvatar
|
||||
initialAvatarUrl={this.state.avatarUrl} />
|
||||
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
|
||||
{ this.state.errorString }
|
||||
</div>
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
+202
-299
@@ -1,8 +1,5 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015, 2016, 2017, 2018, 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.
|
||||
@@ -18,109 +15,128 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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";
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
import ServerPicker from '../../views/elements/ServerPicker';
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
const PHASE_SERVER_DETAILS = 0;
|
||||
// Show the appropriate registration flow(s) for the server
|
||||
const PHASE_REGISTRATION = 1;
|
||||
interface IProps {
|
||||
serverConfig: ValidatedServerConfig;
|
||||
defaultDeviceDisplayName: string;
|
||||
email?: string;
|
||||
brand?: string;
|
||||
clientSecret?: string;
|
||||
sessionId?: string;
|
||||
idSid?: string;
|
||||
fragmentAfterLogin?: string;
|
||||
|
||||
// Enable phases for registration
|
||||
const PHASES_ENABLED = true;
|
||||
// Called when the user has logged in. Params:
|
||||
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
||||
// - The user's password, if available and applicable (may be cached in memory
|
||||
// for a short time so the user is not required to re-enter their password
|
||||
// for operations like uploading cross-signing keys).
|
||||
onLoggedIn(params: {
|
||||
userId: string;
|
||||
deviceId: string
|
||||
homeserverUrl: string;
|
||||
identityServerUrl?: string;
|
||||
accessToken: string;
|
||||
}, password: string): void;
|
||||
makeRegistrationUrl(params: {
|
||||
/* eslint-disable camelcase */
|
||||
client_secret: string;
|
||||
hs_url: string;
|
||||
is_url?: string;
|
||||
session_id: string;
|
||||
/* eslint-enable camelcase */
|
||||
}): void;
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick(): void;
|
||||
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||
}
|
||||
|
||||
export default class Registration extends React.Component {
|
||||
static propTypes = {
|
||||
// Called when the user has logged in. Params:
|
||||
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
||||
// - The user's password, if available and applicable (may be cached in memory
|
||||
// for a short time so the user is not required to re-enter their password
|
||||
// for operations like uploading cross-signing keys).
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
errorText?: ReactNode;
|
||||
// true if we're waiting for the user to complete
|
||||
// We remember the values entered by the user because
|
||||
// the registration form will be unmounted during the
|
||||
// course of registration, but if there's an error we
|
||||
// want to bring back the registration form with the
|
||||
// values the user entered still in it. We can keep
|
||||
// them in this component's state since this component
|
||||
// persist for the duration of the registration process.
|
||||
formVals: Record<string, string>;
|
||||
// user-interactive auth
|
||||
// If we've been given a session ID, we're resuming
|
||||
// straight back into UI auth
|
||||
doingUIAuth: boolean;
|
||||
// If set, we've registered but are not going to log
|
||||
// the user in to their new account automatically.
|
||||
completedNoSignin: boolean;
|
||||
flows: {
|
||||
stages: 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
|
||||
// be seeing.
|
||||
serverIsAlive: boolean;
|
||||
serverErrorIsFatal: boolean;
|
||||
serverDeadError: string;
|
||||
|
||||
clientSecret: PropTypes.string,
|
||||
sessionId: PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
idSid: PropTypes.string,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
};
|
||||
// Our matrix client - part of state because we can't render the UI auth
|
||||
// component without it.
|
||||
matrixClient?: MatrixClient;
|
||||
// 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;
|
||||
}
|
||||
|
||||
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,
|
||||
// We remember the values entered by the user because
|
||||
// the registration form will be unmounted during the
|
||||
// course of registration, but if there's an error we
|
||||
// want to bring back the registration form with the
|
||||
// values the user entered still in it. We can keep
|
||||
// them in this component's state since this component
|
||||
// persist for the duration of the registration process.
|
||||
formVals: {
|
||||
email: this.props.email,
|
||||
},
|
||||
// true if we're waiting for the user to complete
|
||||
// user-interactive auth
|
||||
// If we've been given a session ID, we're resuming
|
||||
// straight back into UI auth
|
||||
doingUIAuth: Boolean(this.props.sessionId),
|
||||
serverType,
|
||||
// Phase of the overall registration dialog.
|
||||
phase: PHASE_REGISTRATION,
|
||||
flows: null,
|
||||
// If set, we've registered but are not going to log
|
||||
// the user in to their new account automatically.
|
||||
completedNoSignin: false,
|
||||
|
||||
// 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
|
||||
// be seeing.
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
|
||||
// Our matrix client - part of state because we can't render the UI auth
|
||||
// component without it.
|
||||
matrixClient: null,
|
||||
|
||||
// whether the HS requires an ID server to register with a threepid
|
||||
serverRequiresIdServer: null,
|
||||
|
||||
// The user ID we've just registered
|
||||
registeredUsername: null,
|
||||
|
||||
// if a different user ID to the one we just registered is logged in,
|
||||
// this is the user ID that's logged in.
|
||||
differentLoggedInUserId: null,
|
||||
};
|
||||
|
||||
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||
this.loginLogic = new Login(hsUrl, isUrl, null, {
|
||||
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
this._replaceClient();
|
||||
this.replaceClient(this.props.serverConfig);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
@@ -129,63 +145,10 @@ export default class Registration extends React.Component {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
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: this.getDefaultPhaseForServerType(serverType),
|
||||
});
|
||||
}
|
||||
this.replaceClient(newProps.serverConfig);
|
||||
}
|
||||
|
||||
getDefaultPhaseForServerType(type) {
|
||||
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_SERVER_DETAILS;
|
||||
}
|
||||
}
|
||||
|
||||
onServerTypeChange = type => {
|
||||
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: this.getDefaultPhaseForServerType(type),
|
||||
});
|
||||
};
|
||||
|
||||
async _replaceClient(serverConfig) {
|
||||
private async replaceClient(serverConfig: ValidatedServerConfig) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
serverDeadError: null,
|
||||
@@ -194,7 +157,6 @@ export default class Registration extends React.Component {
|
||||
// the UI auth component while we don't have a matrix client)
|
||||
busy: true,
|
||||
});
|
||||
if (!serverConfig) serverConfig = this.props.serverConfig;
|
||||
|
||||
// Do a liveliness check on the URLs
|
||||
try {
|
||||
@@ -222,16 +184,20 @@ export default class Registration extends React.Component {
|
||||
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) => {
|
||||
@@ -246,7 +212,7 @@ export default class Registration extends React.Component {
|
||||
// do SSO instead. If we've already started the UI Auth process though, we don't
|
||||
// need to.
|
||||
if (!this.state.doingUIAuth) {
|
||||
await this._makeRegisterRequest(null);
|
||||
await this.makeRegisterRequest(null);
|
||||
// This should never succeed since we specified no auth object.
|
||||
console.log("Expecting 401 from register request but got success!");
|
||||
}
|
||||
@@ -259,26 +225,16 @@ export default class Registration extends React.Component {
|
||||
// At this point registration is pretty much disabled, but before we do that let's
|
||||
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
||||
// the user off to the login page to figure their account out.
|
||||
try {
|
||||
const loginLogic = new Login(hsUrl, isUrl, null, {
|
||||
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
|
||||
if (ssoFlow) {
|
||||
// Redirect to login page - server probably expects SSO only
|
||||
dis.dispatch({action: 'start_login'});
|
||||
} else {
|
||||
this.setState({
|
||||
serverErrorIsFatal: true, // fatal because user cannot continue on this server
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
const flows = await loginLogic.getFlows();
|
||||
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
|
||||
if (hasSsoFlow) {
|
||||
// Redirect to login page - server probably expects SSO only
|
||||
dis.dispatch({action: 'start_login'});
|
||||
} else {
|
||||
this.setState({
|
||||
serverErrorIsFatal: true, // fatal because user cannot continue on this server
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get login flows to check for SSO support", e);
|
||||
showGenericError(e);
|
||||
}
|
||||
} else {
|
||||
console.log("Unable to query for supported registration methods.", e);
|
||||
@@ -287,7 +243,7 @@ export default class Registration extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onFormSubmit = formVals => {
|
||||
private onFormSubmit = formVals => {
|
||||
this.setState({
|
||||
errorText: "",
|
||||
busy: true,
|
||||
@@ -296,7 +252,7 @@ export default class Registration extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
_requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => {
|
||||
private requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => {
|
||||
return this.state.matrixClient.requestRegisterEmailToken(
|
||||
emailAddress,
|
||||
clientSecret,
|
||||
@@ -310,28 +266,26 @@ export default class Registration extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_onUIAuthFinished = async (success, response, extra) => {
|
||||
private onUIAuthFinished = async (success, response, extra) => {
|
||||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
response.data.admin_contact,
|
||||
{
|
||||
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
|
||||
'': _td("This homeserver has exceeded one of its resource limits."),
|
||||
},
|
||||
);
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
response.data.admin_contact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to continue using this service."),
|
||||
},
|
||||
);
|
||||
msg = <div>
|
||||
<p>{errorTop}</p>
|
||||
<p>{errorDetail}</p>
|
||||
@@ -339,11 +293,13 @@ export default class Registration extends React.Component {
|
||||
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdnAvailable = false;
|
||||
for (const flow of response.available_flows) {
|
||||
msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
msisdnAvailable = msisdnAvailable || flow.stages.includes('m.login.msisdn');
|
||||
}
|
||||
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,
|
||||
@@ -358,6 +314,10 @@ export default class Registration extends React.Component {
|
||||
const newState = {
|
||||
doingUIAuth: false,
|
||||
registeredUsername: response.user_id,
|
||||
differentLoggedInUserId: null,
|
||||
completedNoSignin: false,
|
||||
// we're still busy until we get unmounted: don't show the registration form again
|
||||
busy: true,
|
||||
};
|
||||
|
||||
// The user came in through an email validation link. To avoid overwriting
|
||||
@@ -372,8 +332,6 @@ export default class Registration extends React.Component {
|
||||
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
|
||||
);
|
||||
newState.differentLoggedInUserId = sessionOwner;
|
||||
} else {
|
||||
newState.differentLoggedInUserId = null;
|
||||
}
|
||||
|
||||
if (response.access_token) {
|
||||
@@ -385,9 +343,7 @@ export default class Registration extends React.Component {
|
||||
accessToken: response.access_token,
|
||||
}, this.state.formVals.password);
|
||||
|
||||
this._setupPushers();
|
||||
// we're still busy until we get unmounted: don't show the registration form again
|
||||
newState.busy = true;
|
||||
this.setupPushers();
|
||||
} else {
|
||||
newState.busy = false;
|
||||
newState.completedNoSignin = true;
|
||||
@@ -396,7 +352,7 @@ export default class Registration extends React.Component {
|
||||
this.setState(newState);
|
||||
};
|
||||
|
||||
_setupPushers() {
|
||||
private setupPushers() {
|
||||
if (!this.props.brand) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -419,38 +375,23 @@ export default class Registration extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
onLoginClick = ev => {
|
||||
private onLoginClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onLoginClick();
|
||||
};
|
||||
|
||||
onGoToFormClicked = ev => {
|
||||
private onGoToFormClicked = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._replaceClient();
|
||||
this.replaceClient(this.props.serverConfig);
|
||||
this.setState({
|
||||
busy: false,
|
||||
doingUIAuth: false,
|
||||
phase: PHASE_REGISTRATION,
|
||||
});
|
||||
};
|
||||
|
||||
onServerDetailsNextPhaseClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_REGISTRATION,
|
||||
});
|
||||
};
|
||||
|
||||
onEditServerDetailsClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
};
|
||||
|
||||
_makeRegisterRequest = auth => {
|
||||
private makeRegisterRequest = auth => {
|
||||
// We inhibit login if we're trying to register with an email address: this
|
||||
// avoids a lot of complex race conditions that can occur if we try to log
|
||||
// the user in one one or both of the tabs they might end up with after
|
||||
@@ -466,13 +407,15 @@ export default class Registration extends React.Component {
|
||||
username: this.state.formVals.username,
|
||||
password: this.state.formVals.password,
|
||||
initial_device_display_name: this.props.defaultDeviceDisplayName,
|
||||
auth: undefined,
|
||||
inhibit_login: undefined,
|
||||
};
|
||||
if (auth) registerParams.auth = auth;
|
||||
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
|
||||
return this.state.matrixClient.registerRequest(registerParams);
|
||||
};
|
||||
|
||||
_getUIAuthInputs() {
|
||||
private getUIAuthInputs() {
|
||||
return {
|
||||
emailAddress: this.state.formVals.email,
|
||||
phoneCountry: this.state.formVals.phoneCountry,
|
||||
@@ -483,7 +426,7 @@ export default class Registration extends React.Component {
|
||||
// Links to the login page shown after registration is completed are routed through this
|
||||
// which checks the user hasn't already logged in somewhere else (perhaps we should do
|
||||
// this more generally?)
|
||||
_onLoginClickWithCheck = async ev => {
|
||||
private onLoginClickWithCheck = async ev => {
|
||||
ev.preventDefault();
|
||||
|
||||
const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true});
|
||||
@@ -493,72 +436,7 @@ export default class Registration extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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_SERVER_DETAILS && !this.state.serverErrorIsFatal) {
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const serverDetailsProps = {};
|
||||
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>;
|
||||
}
|
||||
|
||||
renderRegisterComponent() {
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private renderRegisterComponent() {
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const RegistrationForm = sdk.getComponent('auth.RegistrationForm');
|
||||
@@ -566,10 +444,10 @@ export default class Registration extends React.Component {
|
||||
if (this.state.matrixClient && this.state.doingUIAuth) {
|
||||
return <InteractiveAuth
|
||||
matrixClient={this.state.matrixClient}
|
||||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={this._getUIAuthInputs()}
|
||||
requestEmailToken={this._requestEmailToken}
|
||||
makeRequest={this.makeRegisterRequest}
|
||||
onAuthFinished={this.onUIAuthFinished}
|
||||
inputs={this.getUIAuthInputs()}
|
||||
requestEmailToken={this.requestEmailToken}
|
||||
sessionId={this.props.sessionId}
|
||||
clientSecret={this.props.clientSecret}
|
||||
emailSid={this.props.idSid}
|
||||
@@ -582,30 +460,48 @@ export default class Registration extends React.Component {
|
||||
<Spinner />
|
||||
</div>;
|
||||
} else if (this.state.flows.length) {
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed and we haven't selected the Free server type, wire
|
||||
// up the server details edit link.
|
||||
if (
|
||||
PHASES_ENABLED &&
|
||||
!SdkConfig.get()['disable_custom_urls'] &&
|
||||
this.state.serverType !== ServerType.FREE
|
||||
) {
|
||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
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 <RegistrationForm
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
flows={this.state.flows}
|
||||
serverConfig={this.props.serverConfig}
|
||||
canSubmit={!this.state.serverErrorIsFatal}
|
||||
serverRequiresIdServer={this.state.serverRequiresIdServer}
|
||||
/>;
|
||||
return <React.Fragment>
|
||||
{ ssoSection }
|
||||
<RegistrationForm
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
flows={this.state.flows}
|
||||
serverConfig={this.props.serverConfig}
|
||||
canSubmit={!this.state.serverErrorIsFatal}
|
||||
/>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,13 +530,15 @@ export default class Registration extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
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>;
|
||||
@@ -658,7 +556,7 @@ export default class Registration extends React.Component {
|
||||
loggedInUserId: this.state.differentLoggedInUserId,
|
||||
},
|
||||
)}</p>
|
||||
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this._onLoginClickWithCheck}>
|
||||
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this.onLoginClickWithCheck}>
|
||||
{_t("Continue with previous account")}
|
||||
</AccessibleButton></p>
|
||||
</div>;
|
||||
@@ -667,7 +565,7 @@ export default class Registration extends React.Component {
|
||||
regDoneText = <h3>{_t(
|
||||
"<a>Log in</a> to your new account.", {},
|
||||
{
|
||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||
a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
|
||||
},
|
||||
)}</h3>;
|
||||
} else {
|
||||
@@ -677,7 +575,7 @@ export default class Registration extends React.Component {
|
||||
regDoneText = <h3>{_t(
|
||||
"You can now close this window or <a>log in</a> to your new account.", {},
|
||||
{
|
||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||
a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
|
||||
},
|
||||
)}</h3>;
|
||||
}
|
||||
@@ -687,10 +585,15 @@ export default class Registration extends React.Component {
|
||||
</div>;
|
||||
} else {
|
||||
body = <div>
|
||||
<h2>{ _t('Create your account') }</h2>
|
||||
<h2>{ _t('Create account') }</h2>
|
||||
{ errorText }
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
<ServerPicker
|
||||
title={_t("Host account on")}
|
||||
dialogTitle={_t("Decide where your account is hosted")}
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.state.doingUIAuth ? undefined : this.props.onServerConfigChange}
|
||||
/>
|
||||
{ this.renderRegisterComponent() }
|
||||
{ goBack }
|
||||
{ signIn }
|
||||
@@ -24,8 +24,8 @@ import Modal from '../../../Modal';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {sendLoginRequest} from "../../../Login";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
import SSOButtons from "../../views/elements/SSOButtons";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
@@ -101,10 +101,11 @@ export default class SoftLogout extends React.Component {
|
||||
// Note: we don't use the existing Login class because it is heavily flow-based. We don't
|
||||
// care about login flows here, unless it is the single flow we support.
|
||||
const client = MatrixClientPeg.get();
|
||||
const loginViews = (await client.loginFlows()).flows.map(f => FLOWS_TO_VIEWS[f.type]);
|
||||
const flows = (await client.loginFlows()).flows;
|
||||
const loginViews = flows.map(f => FLOWS_TO_VIEWS[f.type]);
|
||||
|
||||
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
|
||||
this.setState({loginView: chosenView});
|
||||
this.setState({ flows, loginView: chosenView });
|
||||
}
|
||||
|
||||
onPasswordChange = (ev) => {
|
||||
@@ -240,13 +241,18 @@ export default class SoftLogout extends React.Component {
|
||||
introText = _t("Sign in and regain access to your account.");
|
||||
} // else we already have a message and should use it (key backup warning)
|
||||
|
||||
const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso";
|
||||
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{introText}</p>
|
||||
<SSOButton
|
||||
<SSOButtons
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"}
|
||||
flow={flow}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -102,6 +102,10 @@ export default class CaptchaForm extends React.Component {
|
||||
console.log("Loaded recaptcha script.");
|
||||
try {
|
||||
this._renderRecaptcha(DIV_ID);
|
||||
// clear error if re-rendered
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_loaded");
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default class CustomServerDialog extends React.Component {
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
{ _t("Custom Server Options") }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t(
|
||||
"You can use the custom server options to sign into other " +
|
||||
"Matrix servers by specifying a different homeserver URL. This " +
|
||||
"allows you to use %(brand)s with an existing Matrix account on a " +
|
||||
"different homeserver.",
|
||||
{ brand },
|
||||
)}</p>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||
{ _t("Dismiss") }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ limitations under the License.
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
@@ -421,12 +420,12 @@ export class EmailIdentityAuthEntry extends React.Component {
|
||||
return <Spinner />;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t("An email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
|
||||
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
||||
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please check your email to continue registration.") }</p>
|
||||
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -500,17 +499,11 @@ export class MsisdnAuthEntry extends React.Component {
|
||||
});
|
||||
|
||||
try {
|
||||
const requiresIdServerParam =
|
||||
await this.props.matrixClient.doesServerRequireIdServerParam();
|
||||
let result;
|
||||
if (this._submitUrl) {
|
||||
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else if (requiresIdServerParam) {
|
||||
result = await this.props.matrixClient.submitMsisdnToken(
|
||||
this._sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else {
|
||||
throw new Error("The registration with MSISDN flow is misconfigured");
|
||||
}
|
||||
@@ -519,12 +512,6 @@ export class MsisdnAuthEntry extends React.Component {
|
||||
sid: this._sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
};
|
||||
if (requiresIdServerParam) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.props.matrixClient.getIdentityServerUrl(),
|
||||
);
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
}
|
||||
this.props.submitAuthDict({
|
||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import ServerConfig from "./ServerConfig";
|
||||
|
||||
const MODULAR_URL = 'https://element.io/matrix-services' +
|
||||
'?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
|
||||
|
||||
// TODO: TravisR - Can this extend ServerConfig for most things?
|
||||
|
||||
/*
|
||||
* Configure the Modular server name.
|
||||
*
|
||||
* This is a variant of ServerConfig with only the HS field and different body
|
||||
* text that is specific to the Modular case.
|
||||
*/
|
||||
export default class ModularServerConfig extends ServerConfig {
|
||||
static propTypes = ServerConfig.propTypes;
|
||||
|
||||
async validateAndApplyServer(hsUrl, isUrl) {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hsUrl,
|
||||
isUrl,
|
||||
busy: true,
|
||||
errorText: "",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async validateServer() {
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const submitButton = this.props.submitText
|
||||
? <AccessibleButton
|
||||
element="button"
|
||||
type="submit"
|
||||
className={this.props.submitClass}
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx_ServerConfig">
|
||||
<h3>{_t("Your server")}</h3>
|
||||
{_t(
|
||||
"Enter the location of your Element Matrix Services homeserver. It may use your own " +
|
||||
"domain name or be a subdomain of <a>element.io</a>.",
|
||||
{}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
)}
|
||||
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field
|
||||
id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
/>
|
||||
</div>
|
||||
{submitButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,9 @@ import zxcvbn from "zxcvbn";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import Field from "../elements/Field";
|
||||
import Field, {IInputProps} from "../elements/Field";
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends Omit<IInputProps, "onValidate"> {
|
||||
autoFocus?: boolean;
|
||||
id?: string;
|
||||
className?: string;
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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 classNames from 'classnames';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
*/
|
||||
export default class PasswordLogin extends React.Component {
|
||||
static propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onError: PropTypes.func,
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
initialPhoneNumber: PropTypes.string,
|
||||
initialPassword: PropTypes.string,
|
||||
onUsernameChanged: PropTypes.func,
|
||||
onPhoneCountryChanged: PropTypes.func,
|
||||
onPhoneNumberChanged: PropTypes.func,
|
||||
onPasswordChanged: PropTypes.func,
|
||||
loginIncorrect: PropTypes.bool,
|
||||
disableSubmit: PropTypes.bool,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
busy: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onEditServerDetailsClick: null,
|
||||
onUsernameChanged: function() {},
|
||||
onUsernameBlur: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
onPhoneNumberBlur: function() {},
|
||||
initialUsername: "",
|
||||
initialPhoneCountry: "",
|
||||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
disableSubmit: false,
|
||||
};
|
||||
|
||||
static LOGIN_FIELD_EMAIL = "login_field_email";
|
||||
static LOGIN_FIELD_MXID = "login_field_mxid";
|
||||
static LOGIN_FIELD_PHONE = "login_field_phone";
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: this.props.initialUsername,
|
||||
password: this.props.initialPassword,
|
||||
phoneCountry: this.props.initialPhoneCountry,
|
||||
phoneNumber: this.props.initialPhoneNumber,
|
||||
loginType: PasswordLogin.LOGIN_FIELD_MXID,
|
||||
};
|
||||
|
||||
this.onForgotPasswordClick = this.onForgotPasswordClick.bind(this);
|
||||
this.onSubmitForm = this.onSubmitForm.bind(this);
|
||||
this.onUsernameChanged = this.onUsernameChanged.bind(this);
|
||||
this.onUsernameBlur = this.onUsernameBlur.bind(this);
|
||||
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
|
||||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||
this.onPhoneNumberBlur = this.onPhoneNumberBlur.bind(this);
|
||||
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
||||
this.isLoginEmpty = this.isLoginEmpty.bind(this);
|
||||
}
|
||||
|
||||
onForgotPasswordClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onForgotPasswordClick();
|
||||
}
|
||||
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
let username = ''; // XXX: Synapse breaks if you send null here:
|
||||
let phoneCountry = null;
|
||||
let phoneNumber = null;
|
||||
let error;
|
||||
|
||||
switch (this.state.loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The email field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The username field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
phoneCountry = this.state.phoneCountry;
|
||||
phoneNumber = this.state.phoneNumber;
|
||||
if (!phoneNumber) {
|
||||
error = _t('The phone number field must not be blank.');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.props.onError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.password) {
|
||||
this.props.onError(_t('The password field must not be blank.'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(
|
||||
username,
|
||||
phoneCountry,
|
||||
phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
}
|
||||
|
||||
onUsernameChanged(ev) {
|
||||
this.setState({username: ev.target.value});
|
||||
this.props.onUsernameChanged(ev.target.value);
|
||||
}
|
||||
|
||||
onUsernameFocus() {
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) {
|
||||
CountlyAnalytics.instance.track("onboarding_login_mxid_focus");
|
||||
} else {
|
||||
CountlyAnalytics.instance.track("onboarding_login_email_focus");
|
||||
}
|
||||
}
|
||||
|
||||
onUsernameBlur(ev) {
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) {
|
||||
CountlyAnalytics.instance.track("onboarding_login_mxid_blur");
|
||||
} else {
|
||||
CountlyAnalytics.instance.track("onboarding_login_email_blur");
|
||||
}
|
||||
this.props.onUsernameBlur(ev.target.value);
|
||||
}
|
||||
|
||||
onLoginTypeChange(ev) {
|
||||
const loginType = ev.target.value;
|
||||
this.props.onError(null); // send a null error to clear any error messages
|
||||
this.setState({
|
||||
loginType: loginType,
|
||||
username: "", // Reset because email and username use the same state
|
||||
});
|
||||
CountlyAnalytics.instance.track("onboarding_login_type_changed", { loginType });
|
||||
}
|
||||
|
||||
onPhoneCountryChanged(country) {
|
||||
this.setState({
|
||||
phoneCountry: country.iso2,
|
||||
phonePrefix: country.prefix,
|
||||
});
|
||||
this.props.onPhoneCountryChanged(country.iso2);
|
||||
}
|
||||
|
||||
onPhoneNumberChanged(ev) {
|
||||
this.setState({phoneNumber: ev.target.value});
|
||||
this.props.onPhoneNumberChanged(ev.target.value);
|
||||
}
|
||||
|
||||
onPhoneNumberFocus() {
|
||||
CountlyAnalytics.instance.track("onboarding_login_phone_number_focus");
|
||||
}
|
||||
|
||||
onPhoneNumberBlur(ev) {
|
||||
this.props.onPhoneNumberBlur(ev.target.value);
|
||||
CountlyAnalytics.instance.track("onboarding_login_phone_number_blur");
|
||||
}
|
||||
|
||||
onPasswordChanged(ev) {
|
||||
this.setState({password: ev.target.value});
|
||||
this.props.onPasswordChanged(ev.target.value);
|
||||
}
|
||||
|
||||
renderLoginField(loginType, autoFocus) {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
const classes = {};
|
||||
|
||||
switch (loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="email_input"
|
||||
type="text"
|
||||
label={_t("Email")}
|
||||
placeholder="joe@example.com"
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onFocus={this.onUsernameFocus}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="username_input"
|
||||
type="text"
|
||||
label={_t("Username")}
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onFocus={this.onUsernameFocus}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
|
||||
|
||||
const phoneCountry = <CountryDropdown
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
onOptionChange={this.onPhoneCountryChanged}
|
||||
/>;
|
||||
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
name="phoneNumber"
|
||||
key="phone_input"
|
||||
type="text"
|
||||
label={_t("Phone")}
|
||||
value={this.state.phoneNumber}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
onFocus={this.onPhoneNumberFocus}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isLoginEmpty() {
|
||||
switch (this.state.loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
return !this.state.username;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
return !this.state.phoneCountry || !this.state.phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
||||
|
||||
let forgotPasswordJsx;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = <span>
|
||||
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
|
||||
a: sub => (
|
||||
<AccessibleButton
|
||||
className="mx_Login_forgot"
|
||||
disabled={this.props.busy}
|
||||
kind="link"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
</span>;
|
||||
}
|
||||
|
||||
const pwFieldClass = classNames({
|
||||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
||||
// If login is empty, autoFocus login, otherwise autoFocus password.
|
||||
// this is for when auto server discovery remounts us when the user tries to tab from username to password
|
||||
const autoFocusPassword = !this.isLoginEmpty();
|
||||
const loginField = this.renderLoginField(this.state.loginType, !autoFocusPassword);
|
||||
|
||||
let loginType;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
loginType = (
|
||||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Field
|
||||
element="select"
|
||||
value={this.state.loginType}
|
||||
onChange={this.onLoginTypeChange}
|
||||
disabled={this.props.disableSubmit}
|
||||
>
|
||||
<option
|
||||
key={PasswordLogin.LOGIN_FIELD_MXID}
|
||||
value={PasswordLogin.LOGIN_FIELD_MXID}
|
||||
>
|
||||
{_t('Username')}
|
||||
</option>
|
||||
<option
|
||||
key={PasswordLogin.LOGIN_FIELD_EMAIL}
|
||||
value={PasswordLogin.LOGIN_FIELD_EMAIL}
|
||||
>
|
||||
{_t('Email address')}
|
||||
</option>
|
||||
<option
|
||||
key={PasswordLogin.LOGIN_FIELD_PHONE}
|
||||
value={PasswordLogin.LOGIN_FIELD_PHONE}
|
||||
>
|
||||
{_t('Phone')}
|
||||
</option>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SignInToText serverConfig={this.props.serverConfig}
|
||||
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{loginType}
|
||||
{loginField}
|
||||
<Field
|
||||
className={pwFieldClass}
|
||||
type="password"
|
||||
name="password"
|
||||
label={_t('Password')}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChanged}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocusPassword}
|
||||
/>
|
||||
{forgotPasswordJsx}
|
||||
{ !this.props.busy && <input className="mx_Login_submit"
|
||||
type="submit"
|
||||
value={_t('Sign in')}
|
||||
disabled={this.props.disableSubmit}
|
||||
/> }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
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.
|
||||
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 classNames from 'classnames';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import withValidation from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import Field from "../elements/Field";
|
||||
import CountryDropdown from "./CountryDropdown";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
||||
interface IProps {
|
||||
username: string; // also used for email address
|
||||
phoneCountry: string;
|
||||
phoneNumber: string;
|
||||
|
||||
serverConfig: ValidatedServerConfig;
|
||||
loginIncorrect?: boolean;
|
||||
disableSubmit?: boolean;
|
||||
busy?: boolean;
|
||||
|
||||
onSubmit(username: string, phoneCountry: void, phoneNumber: void, password: string): void;
|
||||
onSubmit(username: void, phoneCountry: string, phoneNumber: string, password: string): void;
|
||||
onUsernameChanged?(username: string): void;
|
||||
onUsernameBlur?(username: string): void;
|
||||
onPhoneCountryChanged?(phoneCountry: string): void;
|
||||
onPhoneNumberChanged?(phoneNumber: string): void;
|
||||
onForgotPasswordClick?(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
fieldValid: Partial<Record<LoginField, boolean>>;
|
||||
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone,
|
||||
password: "",
|
||||
}
|
||||
|
||||
enum LoginField {
|
||||
Email = "login_field_email",
|
||||
MatrixId = "login_field_mxid",
|
||||
Phone = "login_field_phone",
|
||||
Password = "login_field_phone",
|
||||
}
|
||||
|
||||
/*
|
||||
* A pure UI component which displays a username/password form.
|
||||
* The email/username/phone fields are fully-controlled, the password field is not.
|
||||
*/
|
||||
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onUsernameChanged: function() {},
|
||||
onUsernameBlur: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
loginIncorrect: false,
|
||||
disableSubmit: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
// Field error codes by field ID
|
||||
fieldValid: {},
|
||||
loginType: LoginField.MatrixId,
|
||||
password: "",
|
||||
};
|
||||
}
|
||||
|
||||
private onForgotPasswordClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onForgotPasswordClick();
|
||||
};
|
||||
|
||||
private onSubmitForm = async ev => {
|
||||
ev.preventDefault();
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
|
||||
return;
|
||||
}
|
||||
|
||||
let username = ''; // XXX: Synapse breaks if you send null here:
|
||||
let phoneCountry = null;
|
||||
let phoneNumber = null;
|
||||
|
||||
switch (this.state.loginType) {
|
||||
case LoginField.Email:
|
||||
case LoginField.MatrixId:
|
||||
username = this.props.username;
|
||||
break;
|
||||
case LoginField.Phone:
|
||||
phoneCountry = this.props.phoneCountry;
|
||||
phoneNumber = this.props.phoneNumber;
|
||||
break;
|
||||
}
|
||||
|
||||
this.props.onSubmit(username, phoneCountry, phoneNumber, this.state.password);
|
||||
};
|
||||
|
||||
private onUsernameChanged = ev => {
|
||||
this.props.onUsernameChanged(ev.target.value);
|
||||
};
|
||||
|
||||
private onUsernameFocus = () => {
|
||||
if (this.state.loginType === LoginField.MatrixId) {
|
||||
CountlyAnalytics.instance.track("onboarding_login_mxid_focus");
|
||||
} else {
|
||||
CountlyAnalytics.instance.track("onboarding_login_email_focus");
|
||||
}
|
||||
};
|
||||
|
||||
private onUsernameBlur = ev => {
|
||||
if (this.state.loginType === LoginField.MatrixId) {
|
||||
CountlyAnalytics.instance.track("onboarding_login_mxid_blur");
|
||||
} else {
|
||||
CountlyAnalytics.instance.track("onboarding_login_email_blur");
|
||||
}
|
||||
this.props.onUsernameBlur(ev.target.value);
|
||||
};
|
||||
|
||||
private onLoginTypeChange = ev => {
|
||||
const loginType = ev.target.value;
|
||||
this.setState({ loginType });
|
||||
this.props.onUsernameChanged(""); // Reset because email and username use the same state
|
||||
CountlyAnalytics.instance.track("onboarding_login_type_changed", { loginType });
|
||||
};
|
||||
|
||||
private onPhoneCountryChanged = country => {
|
||||
this.props.onPhoneCountryChanged(country.iso2);
|
||||
};
|
||||
|
||||
private onPhoneNumberChanged = ev => {
|
||||
this.props.onPhoneNumberChanged(ev.target.value);
|
||||
};
|
||||
|
||||
private onPhoneNumberFocus = () => {
|
||||
CountlyAnalytics.instance.track("onboarding_login_phone_number_focus");
|
||||
};
|
||||
|
||||
private onPhoneNumberBlur = ev => {
|
||||
CountlyAnalytics.instance.track("onboarding_login_phone_number_blur");
|
||||
};
|
||||
|
||||
private onPasswordChanged = ev => {
|
||||
this.setState({password: ev.target.value});
|
||||
};
|
||||
|
||||
private async verifyFieldsBeforeSubmit() {
|
||||
// Blur the active element if any, so we first run its blur validation,
|
||||
// which is less strict than the pass we're about to do below for all fields.
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
||||
const fieldIDsInDisplayOrder = [
|
||||
this.state.loginType,
|
||||
LoginField.Password,
|
||||
];
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
// values for required fields.
|
||||
for (const fieldID of fieldIDsInDisplayOrder) {
|
||||
const field = this[fieldID];
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
// We must wait for these validations to finish before queueing
|
||||
// up the setState below so our setState goes in the queue after
|
||||
// all the setStates from these validate calls (that's how we
|
||||
// know they've finished).
|
||||
await field.validate({ allowEmpty: false });
|
||||
}
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
|
||||
|
||||
if (!invalidField) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Focus the first invalid field and show feedback in the stricter mode
|
||||
// that no longer allows empty values for required fields.
|
||||
invalidField.focus();
|
||||
invalidField.validate({ allowEmpty: false, focused: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
private allFieldsValid() {
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (!this.state.fieldValid[keys[i]]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private findFirstInvalidField(fieldIDs: LoginField[]) {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private markFieldValid(fieldID: LoginField, valid: boolean) {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
fieldValid,
|
||||
});
|
||||
}
|
||||
|
||||
private validateUsernameRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter username"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onUsernameValidate = async (fieldState) => {
|
||||
const result = await this.validateUsernameRules(fieldState);
|
||||
this.markFieldValid(LoginField.MatrixId, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
private validateEmailRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter email address"),
|
||||
}, {
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onEmailValidate = async (fieldState) => {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
this.markFieldValid(LoginField.Email, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
private validatePhoneNumberRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter phone number"),
|
||||
}, {
|
||||
key: "number",
|
||||
test: ({ value }) => !value || PHONE_NUMBER_REGEX.test(value),
|
||||
invalid: () => _t("That phone number doesn't look quite right, please check and try again"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onPhoneNumberValidate = async (fieldState) => {
|
||||
const result = await this.validatePhoneNumberRules(fieldState);
|
||||
this.markFieldValid(LoginField.Password, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
private validatePasswordRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter password"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private onPasswordValidate = async (fieldState) => {
|
||||
const result = await this.validatePasswordRules(fieldState);
|
||||
this.markFieldValid(LoginField.Password, result.valid);
|
||||
return result;
|
||||
}
|
||||
|
||||
private renderLoginField(loginType: IState["loginType"], autoFocus: boolean) {
|
||||
const classes = {
|
||||
error: false,
|
||||
};
|
||||
|
||||
switch (loginType) {
|
||||
case LoginField.Email:
|
||||
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="email_input"
|
||||
type="text"
|
||||
label={_t("Email")}
|
||||
placeholder="joe@example.com"
|
||||
value={this.props.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onFocus={this.onUsernameFocus}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
onValidate={this.onEmailValidate}
|
||||
ref={field => this[LoginField.Email] = field}
|
||||
/>;
|
||||
case LoginField.MatrixId:
|
||||
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="username_input"
|
||||
type="text"
|
||||
label={_t("Username")}
|
||||
placeholder={_t("Username").toLocaleLowerCase()}
|
||||
value={this.props.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onFocus={this.onUsernameFocus}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
onValidate={this.onUsernameValidate}
|
||||
ref={field => this[LoginField.MatrixId] = field}
|
||||
/>;
|
||||
case LoginField.Phone: {
|
||||
classes.error = this.props.loginIncorrect && !this.props.phoneNumber;
|
||||
|
||||
const phoneCountry = <CountryDropdown
|
||||
value={this.props.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
onOptionChange={this.onPhoneCountryChanged}
|
||||
/>;
|
||||
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
name="phoneNumber"
|
||||
key="phone_input"
|
||||
type="text"
|
||||
label={_t("Phone")}
|
||||
value={this.props.phoneNumber}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
onFocus={this.onPhoneNumberFocus}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
onValidate={this.onPhoneNumberValidate}
|
||||
ref={field => this[LoginField.Password] = field}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isLoginEmpty() {
|
||||
switch (this.state.loginType) {
|
||||
case LoginField.Email:
|
||||
case LoginField.MatrixId:
|
||||
return !this.props.username;
|
||||
case LoginField.Phone:
|
||||
return !this.props.phoneCountry || !this.props.phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let forgotPasswordJsx;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = <AccessibleButton
|
||||
className="mx_Login_forgot"
|
||||
disabled={this.props.busy}
|
||||
kind="link"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
>
|
||||
{_t("Forgot password?")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const pwFieldClass = classNames({
|
||||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
||||
// If login is empty, autoFocus login, otherwise autoFocus password.
|
||||
// this is for when auto server discovery remounts us when the user tries to tab from username to password
|
||||
const autoFocusPassword = !this.isLoginEmpty();
|
||||
const loginField = this.renderLoginField(this.state.loginType, !autoFocusPassword);
|
||||
|
||||
let loginType;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
loginType = (
|
||||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Field
|
||||
element="select"
|
||||
value={this.state.loginType}
|
||||
onChange={this.onLoginTypeChange}
|
||||
disabled={this.props.disableSubmit}
|
||||
>
|
||||
<option key={LoginField.MatrixId} value={LoginField.MatrixId}>
|
||||
{_t('Username')}
|
||||
</option>
|
||||
<option
|
||||
key={LoginField.Email}
|
||||
value={LoginField.Email}
|
||||
>
|
||||
{_t('Email address')}
|
||||
</option>
|
||||
<option key={LoginField.Password} value={LoginField.Password}>
|
||||
{_t('Phone')}
|
||||
</option>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{loginType}
|
||||
{loginField}
|
||||
<Field
|
||||
className={pwFieldClass}
|
||||
type="password"
|
||||
name="password"
|
||||
label={_t('Password')}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChanged}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocusPassword}
|
||||
onValidate={this.onPasswordValidate}
|
||||
ref={field => this[LoginField.Password] = field}
|
||||
/>
|
||||
{forgotPasswordJsx}
|
||||
{ !this.props.busy && <input className="mx_Login_submit"
|
||||
type="submit"
|
||||
value={_t('Sign in')}
|
||||
disabled={this.props.disableSubmit}
|
||||
/> }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+134
-180
@@ -1,8 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2015, 2016, 2017, 2018, 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.
|
||||
@@ -18,7 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import * as Email from '../../../email';
|
||||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||
@@ -30,34 +28,59 @@ 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';
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
const FIELD_USERNAME = 'field_username';
|
||||
const FIELD_PASSWORD = 'field_password';
|
||||
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||
enum RegistrationField {
|
||||
Email = "field_email",
|
||||
PhoneNumber = "field_phone_number",
|
||||
Username = "field_username",
|
||||
Password = "field_password",
|
||||
PasswordConfirm = "field_password_confirm",
|
||||
}
|
||||
|
||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||
|
||||
interface IProps {
|
||||
// Values pre-filled in the input boxes when the component loads
|
||||
defaultEmail?: string;
|
||||
defaultPhoneCountry?: string;
|
||||
defaultPhoneNumber?: string;
|
||||
defaultUsername?: string;
|
||||
defaultPassword?: string;
|
||||
flows: {
|
||||
stages: string[];
|
||||
}[];
|
||||
serverConfig: ValidatedServerConfig;
|
||||
canSubmit?: boolean;
|
||||
|
||||
onRegisterClick(params: {
|
||||
username: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
phoneCountry?: string;
|
||||
phoneNumber?: string;
|
||||
}): Promise<void>;
|
||||
onEditServerDetailsClick?(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// Field error codes by field ID
|
||||
fieldValid: Partial<Record<RegistrationField, boolean>>;
|
||||
// The ISO2 country code selected in the phone number entry
|
||||
phoneCountry: string;
|
||||
username: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
password: string;
|
||||
passwordConfirm: string;
|
||||
passwordComplexity?: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* A pure UI component which displays a registration form.
|
||||
*/
|
||||
export default class RegistrationForm extends React.Component {
|
||||
static propTypes = {
|
||||
// Values pre-filled in the input boxes when the component loads
|
||||
defaultEmail: PropTypes.string,
|
||||
defaultPhoneCountry: PropTypes.string,
|
||||
defaultPhoneNumber: PropTypes.string,
|
||||
defaultUsername: PropTypes.string,
|
||||
defaultPassword: PropTypes.string,
|
||||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
canSubmit: PropTypes.bool,
|
||||
serverRequiresIdServer: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default class RegistrationForm extends React.PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
onValidationChange: console.error,
|
||||
canSubmit: true,
|
||||
@@ -67,9 +90,7 @@ export default class RegistrationForm extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// Field error codes by field ID
|
||||
fieldValid: {},
|
||||
// The ISO2 country code selected in the phone number entry
|
||||
phoneCountry: this.props.defaultPhoneCountry,
|
||||
username: this.props.defaultUsername || "",
|
||||
email: this.props.defaultEmail || "",
|
||||
@@ -82,8 +103,9 @@ export default class RegistrationForm extends React.Component {
|
||||
CountlyAnalytics.instance.track("onboarding_registration_begin");
|
||||
}
|
||||
|
||||
onSubmit = async ev => {
|
||||
private onSubmit = async ev => {
|
||||
ev.preventDefault();
|
||||
ev.persist();
|
||||
|
||||
if (!this.props.canSubmit) return;
|
||||
|
||||
@@ -93,46 +115,31 @@ export default class RegistrationForm extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
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
|
||||
self._doSubmit(ev);
|
||||
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) {
|
||||
self._doSubmit(ev);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
self._doSubmit(ev);
|
||||
this.doSubmit(ev);
|
||||
}
|
||||
};
|
||||
|
||||
_doSubmit(ev) {
|
||||
private doSubmit(ev) {
|
||||
const email = this.state.email.trim();
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_ok", {
|
||||
@@ -155,20 +162,20 @@ export default class RegistrationForm extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
async verifyFieldsBeforeSubmit() {
|
||||
private async verifyFieldsBeforeSubmit() {
|
||||
// Blur the active element if any, so we first run its blur validation,
|
||||
// which is less strict than the pass we're about to do below for all fields.
|
||||
const activeElement = document.activeElement;
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
||||
const fieldIDsInDisplayOrder = [
|
||||
FIELD_USERNAME,
|
||||
FIELD_PASSWORD,
|
||||
FIELD_PASSWORD_CONFIRM,
|
||||
FIELD_EMAIL,
|
||||
FIELD_PHONE_NUMBER,
|
||||
RegistrationField.Username,
|
||||
RegistrationField.Password,
|
||||
RegistrationField.PasswordConfirm,
|
||||
RegistrationField.Email,
|
||||
RegistrationField.PhoneNumber,
|
||||
];
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
@@ -209,7 +216,7 @@ export default class RegistrationForm extends React.Component {
|
||||
/**
|
||||
* @returns {boolean} true if all fields were valid last time they were validated.
|
||||
*/
|
||||
allFieldsValid() {
|
||||
private allFieldsValid() {
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (!this.state.fieldValid[keys[i]]) {
|
||||
@@ -219,7 +226,7 @@ export default class RegistrationForm extends React.Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
findFirstInvalidField(fieldIDs) {
|
||||
private findFirstInvalidField(fieldIDs: RegistrationField[]) {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
@@ -228,7 +235,7 @@ export default class RegistrationForm extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
markFieldValid(fieldID, valid) {
|
||||
private markFieldValid(fieldID: RegistrationField, valid: boolean) {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
@@ -236,26 +243,26 @@ export default class RegistrationForm extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
onEmailChange = ev => {
|
||||
private onEmailChange = ev => {
|
||||
this.setState({
|
||||
email: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onEmailValidate = async fieldState => {
|
||||
private onEmailValidate = async fieldState => {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
this.markFieldValid(FIELD_EMAIL, result.valid);
|
||||
this.markFieldValid(RegistrationField.Email, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validateEmailRules = withValidation({
|
||||
private validateEmailRules = withValidation({
|
||||
description: () => _t("Use an email address to recover your account"),
|
||||
hideDescriptionIfValid: true,
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value;
|
||||
test(this: RegistrationForm, { value, allowEmpty }) {
|
||||
return allowEmpty || !this.authStepIsRequired('m.login.email.identity') || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter email address (required on this homeserver)"),
|
||||
},
|
||||
@@ -267,29 +274,29 @@ export default class RegistrationForm extends React.Component {
|
||||
],
|
||||
});
|
||||
|
||||
onPasswordChange = ev => {
|
||||
private onPasswordChange = ev => {
|
||||
this.setState({
|
||||
password: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onPasswordValidate = result => {
|
||||
this.markFieldValid(FIELD_PASSWORD, result.valid);
|
||||
private onPasswordValidate = result => {
|
||||
this.markFieldValid(RegistrationField.Password, result.valid);
|
||||
};
|
||||
|
||||
onPasswordConfirmChange = ev => {
|
||||
private onPasswordConfirmChange = ev => {
|
||||
this.setState({
|
||||
passwordConfirm: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onPasswordConfirmValidate = async fieldState => {
|
||||
private onPasswordConfirmValidate = async fieldState => {
|
||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||
this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid);
|
||||
this.markFieldValid(RegistrationField.PasswordConfirm, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validatePasswordConfirmRules = withValidation({
|
||||
private validatePasswordConfirmRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
@@ -298,65 +305,64 @@ export default class RegistrationForm extends React.Component {
|
||||
},
|
||||
{
|
||||
key: "match",
|
||||
test({ value }) {
|
||||
test(this: RegistrationForm, { value }) {
|
||||
return !value || value === this.state.password;
|
||||
},
|
||||
invalid: () => _t("Passwords don't match"),
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
onPhoneCountryChange = newVal => {
|
||||
private onPhoneCountryChange = newVal => {
|
||||
this.setState({
|
||||
phoneCountry: newVal.iso2,
|
||||
phonePrefix: newVal.prefix,
|
||||
});
|
||||
};
|
||||
|
||||
onPhoneNumberChange = ev => {
|
||||
private onPhoneNumberChange = ev => {
|
||||
this.setState({
|
||||
phoneNumber: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onPhoneNumberValidate = async fieldState => {
|
||||
private onPhoneNumberValidate = async fieldState => {
|
||||
const result = await this.validatePhoneNumberRules(fieldState);
|
||||
this.markFieldValid(FIELD_PHONE_NUMBER, result.valid);
|
||||
this.markFieldValid(RegistrationField.PhoneNumber, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validatePhoneNumberRules = withValidation({
|
||||
private validatePhoneNumberRules = withValidation({
|
||||
description: () => _t("Other users can invite you to rooms using your contact details"),
|
||||
hideDescriptionIfValid: true,
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test({ value, allowEmpty }) {
|
||||
return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value;
|
||||
test(this: RegistrationForm, { value, allowEmpty }) {
|
||||
return allowEmpty || !this.authStepIsRequired('m.login.msisdn') || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter phone number (required on this homeserver)"),
|
||||
},
|
||||
{
|
||||
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"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onUsernameChange = ev => {
|
||||
private onUsernameChange = ev => {
|
||||
this.setState({
|
||||
username: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onUsernameValidate = async fieldState => {
|
||||
private onUsernameValidate = async fieldState => {
|
||||
const result = await this.validateUsernameRules(fieldState);
|
||||
this.markFieldValid(FIELD_USERNAME, result.valid);
|
||||
this.markFieldValid(RegistrationField.Username, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validateUsernameRules = withValidation({
|
||||
private validateUsernameRules = withValidation({
|
||||
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
|
||||
hideDescriptionIfValid: true,
|
||||
rules: [
|
||||
@@ -379,7 +385,7 @@ export default class RegistrationForm extends React.Component {
|
||||
* @param {string} step A stage name to check
|
||||
* @returns {boolean} Whether it is required
|
||||
*/
|
||||
_authStepIsRequired(step) {
|
||||
private authStepIsRequired(step: string) {
|
||||
return this.props.flows.every((flow) => {
|
||||
return flow.stages.includes(step);
|
||||
});
|
||||
@@ -391,46 +397,36 @@ export default class RegistrationForm extends React.Component {
|
||||
* @param {string} step A stage name to check
|
||||
* @returns {boolean} Whether it is used
|
||||
*/
|
||||
_authStepIsUsed(step) {
|
||||
private authStepIsUsed(step: string) {
|
||||
return this.props.flows.some((flow) => {
|
||||
return flow.stages.includes(step);
|
||||
});
|
||||
}
|
||||
|
||||
_showEmail() {
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
if (
|
||||
(this.props.serverRequiresIdServer && !haveIs) ||
|
||||
!this._authStepIsUsed('m.login.email.identity')
|
||||
) {
|
||||
private showEmail() {
|
||||
if (!this.authStepIsUsed('m.login.email.identity')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_showPhoneNumber() {
|
||||
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;
|
||||
}
|
||||
|
||||
renderEmail() {
|
||||
if (!this._showEmail()) {
|
||||
private renderEmail() {
|
||||
if (!this.showEmail()) {
|
||||
return null;
|
||||
}
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
|
||||
const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ?
|
||||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
return <Field
|
||||
ref={field => this[FIELD_EMAIL] = field}
|
||||
ref={field => this[RegistrationField.Email] = field}
|
||||
type="text"
|
||||
label={emailPlaceholder}
|
||||
value={this.state.email}
|
||||
@@ -441,10 +437,10 @@ export default class RegistrationForm extends React.Component {
|
||||
/>;
|
||||
}
|
||||
|
||||
renderPassword() {
|
||||
private renderPassword() {
|
||||
return <PassphraseField
|
||||
id="mx_RegistrationForm_password"
|
||||
fieldRef={field => this[FIELD_PASSWORD] = field}
|
||||
fieldRef={field => this[RegistrationField.Password] = field}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChange}
|
||||
@@ -455,13 +451,12 @@ export default class RegistrationForm extends React.Component {
|
||||
}
|
||||
|
||||
renderPasswordConfirm() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
id="mx_RegistrationForm_passwordConfirm"
|
||||
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
|
||||
ref={field => this[RegistrationField.PasswordConfirm] = field}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={_t("Confirm")}
|
||||
label={_t("Confirm password")}
|
||||
value={this.state.passwordConfirm}
|
||||
onChange={this.onPasswordConfirmChange}
|
||||
onValidate={this.onPasswordConfirmValidate}
|
||||
@@ -471,12 +466,11 @@ export default class RegistrationForm extends React.Component {
|
||||
}
|
||||
|
||||
renderPhoneNumber() {
|
||||
if (!this._showPhoneNumber()) {
|
||||
if (!this.showPhoneNumber()) {
|
||||
return null;
|
||||
}
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const phoneLabel = this._authStepIsRequired('m.login.msisdn') ?
|
||||
const phoneLabel = this.authStepIsRequired('m.login.msisdn') ?
|
||||
_t("Phone") :
|
||||
_t("Phone (optional)");
|
||||
const phoneCountry = <CountryDropdown
|
||||
@@ -486,7 +480,7 @@ export default class RegistrationForm extends React.Component {
|
||||
onOptionChange={this.onPhoneCountryChange}
|
||||
/>;
|
||||
return <Field
|
||||
ref={field => this[FIELD_PHONE_NUMBER] = field}
|
||||
ref={field => this[RegistrationField.PhoneNumber] = field}
|
||||
type="text"
|
||||
label={phoneLabel}
|
||||
value={this.state.phoneNumber}
|
||||
@@ -497,13 +491,13 @@ export default class RegistrationForm extends React.Component {
|
||||
}
|
||||
|
||||
renderUsername() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
id="mx_RegistrationForm_username"
|
||||
ref={field => this[FIELD_USERNAME] = field}
|
||||
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}
|
||||
@@ -513,72 +507,33 @@ export default class RegistrationForm extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let yourMatrixAccountText = _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>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
if (this.props.onEditServerDetailsClick) {
|
||||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} />
|
||||
);
|
||||
|
||||
let emailHelperText = null;
|
||||
if (this._showEmail()) {
|
||||
if (this._showPhoneNumber()) {
|
||||
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>
|
||||
<h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
{this.renderUsername()}
|
||||
@@ -592,7 +547,6 @@ export default class RegistrationForm extends React.Component {
|
||||
{this.renderPhoneNumber()}
|
||||
</div>
|
||||
{ emailHelperText }
|
||||
{ noIsText }
|
||||
{ registerButton }
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,291 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
import classNames from 'classnames';
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
/*
|
||||
* A pure UI component which displays the HS and IS to use.
|
||||
*/
|
||||
|
||||
export default class ServerConfig extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
// The current configuration that the user is expecting to change.
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
|
||||
// Called after the component calls onServerConfigChange
|
||||
onAfterSubmit: PropTypes.func,
|
||||
|
||||
// Optional text for the submit button. If falsey, no button will be shown.
|
||||
submitText: PropTypes.string,
|
||||
|
||||
// Optional class for the submit button. Only applies if the submit button
|
||||
// is to be rendered.
|
||||
submitClass: PropTypes.string,
|
||||
|
||||
// Whether the flow this component is embedded in requires an identity
|
||||
// server when the homeserver says it will need one. Default false.
|
||||
showIdentityServerIfRequiredByHomeserver: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onServerConfigChange: function() {},
|
||||
delayTimeMs: 0,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
busy: false,
|
||||
errorText: "",
|
||||
hsUrl: props.serverConfig.hsUrl,
|
||||
isUrl: props.serverConfig.isUrl,
|
||||
showIdentityServer: false,
|
||||
};
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_custom_server");
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.state.isUrl) return;
|
||||
|
||||
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
||||
}
|
||||
|
||||
async validateServer() {
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
const result = this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// If the UI flow this component is embedded in requires an identity
|
||||
// server when the homeserver says it will need one, check first and
|
||||
// reveal this field if not already shown.
|
||||
// XXX: This a backward compatibility path for homeservers that require
|
||||
// an identity server to be passed during certain flows.
|
||||
// See also https://github.com/matrix-org/synapse/pull/5868.
|
||||
if (
|
||||
this.props.showIdentityServerIfRequiredByHomeserver &&
|
||||
!this.state.showIdentityServer &&
|
||||
await this.isIdentityServerRequiredByHomeserver()
|
||||
) {
|
||||
this.setState({
|
||||
showIdentityServer: true,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async validateAndApplyServer(hsUrl, isUrl) {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
|
||||
this.setState({
|
||||
hsUrl: defaultConfig.hsUrl,
|
||||
isUrl: defaultConfig.isUrl,
|
||||
busy: false,
|
||||
errorText: "",
|
||||
});
|
||||
this.props.onServerConfigChange(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hsUrl,
|
||||
isUrl,
|
||||
busy: true,
|
||||
errorText: "",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (!stateForError.isFatalError) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
// carry on anyway
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} else {
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async isIdentityServerRequiredByHomeserver() {
|
||||
// XXX: We shouldn't have to create a whole new MatrixClient just to
|
||||
// check if the homeserver requires an identity server... Should it be
|
||||
// extracted to a static utils function...?
|
||||
return createClient({
|
||||
baseUrl: this.state.hsUrl,
|
||||
}).doesServerRequireIdServerParam();
|
||||
}
|
||||
|
||||
onHomeserverBlur = (ev) => {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||
this.validateServer();
|
||||
});
|
||||
};
|
||||
|
||||
onHomeserverChange = (ev) => {
|
||||
const hsUrl = ev.target.value;
|
||||
this.setState({ hsUrl });
|
||||
};
|
||||
|
||||
onIdentityServerBlur = (ev) => {
|
||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
|
||||
this.validateServer();
|
||||
});
|
||||
};
|
||||
|
||||
onIdentityServerChange = (ev) => {
|
||||
const isUrl = ev.target.value;
|
||||
this.setState({ isUrl });
|
||||
};
|
||||
|
||||
onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const result = await this.validateServer();
|
||||
if (!result) return; // Do not continue.
|
||||
|
||||
if (this.props.onAfterSubmit) {
|
||||
this.props.onAfterSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
_waitThenInvoke(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
clearTimeout(existingTimeoutId);
|
||||
}
|
||||
return setTimeout(fn.bind(this), this.props.delayTimeMs);
|
||||
}
|
||||
|
||||
showHelpPopup = () => {
|
||||
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
|
||||
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||
};
|
||||
|
||||
_renderHomeserverSection() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <div>
|
||||
{_t("Enter your custom homeserver URL <a>What does this mean?</a>", {}, {
|
||||
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field
|
||||
id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderIdentityServerSection() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const classes = classNames({
|
||||
"mx_ServerConfig_identityServer": true,
|
||||
"mx_ServerConfig_identityServer_shown": this.state.showIdentityServer,
|
||||
});
|
||||
return <div className={classes}>
|
||||
{_t("Enter your custom identity server URL <a>What does this mean?</a>", {}, {
|
||||
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.serverConfig.isUrl}
|
||||
value={this.state.isUrl || ''}
|
||||
onBlur={this.onIdentityServerBlur}
|
||||
onChange={this.onIdentityServerChange}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const errorText = this.state.errorText
|
||||
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
|
||||
: null;
|
||||
|
||||
const submitButton = this.props.submitText
|
||||
? <AccessibleButton
|
||||
element="button"
|
||||
type="submit"
|
||||
className={this.props.submitClass}
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<form className="mx_ServerConfig" onSubmit={this.onSubmit} autoComplete="off">
|
||||
<h3>{_t("Other servers")}</h3>
|
||||
{errorText}
|
||||
{this._renderHomeserverSection()}
|
||||
{this._renderIdentityServerSection()}
|
||||
{submitButton}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import {makeType} from "../../../utils/TypeUtils";
|
||||
|
||||
const MODULAR_URL = 'https://element.io/matrix-services' +
|
||||
'?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
|
||||
|
||||
export const FREE = 'Free';
|
||||
export const PREMIUM = 'Premium';
|
||||
export const ADVANCED = 'Advanced';
|
||||
|
||||
export const TYPES = {
|
||||
FREE: {
|
||||
id: FREE,
|
||||
label: () => _t('Free'),
|
||||
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
|
||||
description: () => _t('Join millions for free on the largest public server'),
|
||||
serverConfig: makeType(ValidatedServerConfig, {
|
||||
hsUrl: "https://matrix-client.matrix.org",
|
||||
hsName: "matrix.org",
|
||||
hsNameIsDifferent: false,
|
||||
isUrl: "https://vector.im",
|
||||
}),
|
||||
},
|
||||
PREMIUM: {
|
||||
id: PREMIUM,
|
||||
label: () => _t('Premium'),
|
||||
logo: () => <img src={require('../../../../res/img/ems-logo.svg')} height={16} />,
|
||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
}),
|
||||
identityServerUrl: "https://vector.im",
|
||||
},
|
||||
ADVANCED: {
|
||||
id: ADVANCED,
|
||||
label: () => _t('Advanced'),
|
||||
logo: () => <div>
|
||||
<img src={require('../../../../res/img/feather-customised/globe.svg')} />
|
||||
{_t('Other')}
|
||||
</div>,
|
||||
description: () => _t('Find other public servers or use a custom server'),
|
||||
},
|
||||
};
|
||||
|
||||
export function getTypeFromServerConfig(config) {
|
||||
const {hsUrl} = config;
|
||||
if (!hsUrl) {
|
||||
return null;
|
||||
} else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
|
||||
return FREE;
|
||||
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
|
||||
// This is an unlikely case to reach, as Modular defaults to hiding the
|
||||
// server type selector.
|
||||
return PREMIUM;
|
||||
} else {
|
||||
return ADVANCED;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ServerTypeSelector extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The default selected type.
|
||||
selected: PropTypes.string,
|
||||
// Handler called when the selected type changes.
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
selected,
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
selected,
|
||||
};
|
||||
}
|
||||
|
||||
updateSelectedType(type) {
|
||||
if (this.state.selected === type) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selected: type,
|
||||
});
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(type);
|
||||
}
|
||||
}
|
||||
|
||||
onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
const type = e.currentTarget.dataset.id;
|
||||
this.updateSelectedType(type);
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const serverTypes = [];
|
||||
for (const type of Object.values(TYPES)) {
|
||||
const { id, label, logo, description } = type;
|
||||
const classes = classnames(
|
||||
"mx_ServerTypeSelector_type",
|
||||
`mx_ServerTypeSelector_type_${id}`,
|
||||
{
|
||||
"mx_ServerTypeSelector_type_selected": id === this.state.selected,
|
||||
},
|
||||
);
|
||||
|
||||
serverTypes.push(<div className={classes} key={id} >
|
||||
<div className="mx_ServerTypeSelector_label">
|
||||
{label()}
|
||||
</div>
|
||||
<AccessibleButton onClick={this.onClick} data-id={id}>
|
||||
<div className="mx_ServerTypeSelector_logo">
|
||||
{logo()}
|
||||
</div>
|
||||
<div className="mx_ServerTypeSelector_description">
|
||||
{description()}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return <div className="mx_ServerTypeSelector">
|
||||
{serverTypes}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import PropTypes from "prop-types";
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
export default class SignInToText extends React.PureComponent {
|
||||
static propTypes = {
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
if (this.props.onEditServerDetailsClick) {
|
||||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
return <h3>
|
||||
{signInToText}
|
||||
{editLink}
|
||||
</h3>;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +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';
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
const PulsedAvatar: React.FC<IProps> = (props) => {
|
||||
return <div className="mx_PulsedAvatar">
|
||||
{props.children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default PulsedAvatar;
|
||||
@@ -149,7 +149,7 @@ export default class MessageContextMenu extends React.Component {
|
||||
onRedactClick = () => {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: async (proceed) => {
|
||||
onFinished: async (proceed, reason) => {
|
||||
if (!proceed) return;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
@@ -157,6 +157,8 @@ export default class MessageContextMenu extends React.Component {
|
||||
await cli.redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
undefined,
|
||||
reason ? { reason } : {},
|
||||
);
|
||||
} catch (e) {
|
||||
const code = e.errcode || e.statusCode;
|
||||
|
||||
@@ -57,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
let unpinButton;
|
||||
if (showUnpin) {
|
||||
const onUnpinClick = () => {
|
||||
WidgetStore.instance.unpinWidget(app.id);
|
||||
WidgetStore.instance.unpinWidget(room.roomId, app.id);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
@@ -143,7 +143,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
let moveLeftButton;
|
||||
if (showUnpin && widgetIndex > 0) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(app.id, -1);
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, -1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
@@ -153,7 +153,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||
let moveRightButton;
|
||||
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(app.id, 1);
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, 1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
||||
@@ -23,15 +23,17 @@ import { _t } from '../../../languageHandler';
|
||||
*/
|
||||
export default class ConfirmRedactDialog extends React.Component {
|
||||
render() {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
|
||||
return (
|
||||
<QuestionDialog onFinished={this.props.onFinished}
|
||||
<TextInputDialog onFinished={this.props.onFinished}
|
||||
title={_t("Confirm Removal")}
|
||||
description={
|
||||
_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
placeholder={_t("Reason (optional)")}
|
||||
focus
|
||||
button={_t("Remove")}>
|
||||
</QuestionDialog>
|
||||
</TextInputDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export default class InfoDialog extends React.Component {
|
||||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
onKeyDown: PropTypes.func,
|
||||
fixedWidth: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -54,6 +55,7 @@ export default class InfoDialog extends React.Component {
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={this.props.hasCloseButton}
|
||||
onKeyDown={this.props.onKeyDown}
|
||||
fixedWidth={this.props.fixedWidth}
|
||||
>
|
||||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
|
||||
@@ -31,7 +31,7 @@ import dis from "../../../dispatcher/dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
@@ -41,6 +41,7 @@ import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
@@ -308,10 +309,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: PropTypes.string,
|
||||
|
||||
// Initial value to populate the filter with
|
||||
initialText: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
kind: KIND_DM,
|
||||
initialText: "",
|
||||
};
|
||||
|
||||
_debounceTimer: number = null;
|
||||
@@ -338,7 +343,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
this.state = {
|
||||
targets: [], // array of Member objects (see interface above)
|
||||
filterText: "",
|
||||
filterText: this.props.initialText,
|
||||
recents: InviteDialog.buildRecents(alreadyInvited),
|
||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||
suggestions: this._buildSuggestions(alreadyInvited),
|
||||
@@ -356,6 +361,12 @@ export default class InviteDialog extends React.PureComponent {
|
||||
this._editorRef = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.initialText) {
|
||||
this._updateSuggestions(this.props.initialText);
|
||||
}
|
||||
}
|
||||
|
||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
@@ -575,7 +586,12 @@ export default class InviteDialog extends React.PureComponent {
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||
let existingRoom: Room;
|
||||
if (targetIds.length === 1) {
|
||||
existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]);
|
||||
} else {
|
||||
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||
}
|
||||
if (existingRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
@@ -687,6 +703,115 @@ export default class InviteDialog extends React.PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_updateSuggestions = async (term) => {
|
||||
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||
if (term !== this.state.filterText) {
|
||||
// Discard the results - we were probably too slow on the server-side to make
|
||||
// these results useful. This is a race we want to avoid because we could overwrite
|
||||
// more accurate results.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!r.results) r.results = [];
|
||||
|
||||
// While we're here, try and autocomplete a search result for the mxid itself
|
||||
// if there's no matches (and the input looks like a mxid).
|
||||
if (term[0] === '@' && term.indexOf(':') > 1) {
|
||||
try {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
||||
if (profile) {
|
||||
// If we have a profile, we have enough information to assume that
|
||||
// the mxid can be invited - add it to the list. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: profile['displayname'],
|
||||
avatar_url: profile['avatar_url'],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Non-fatal error trying to make an invite for a user ID");
|
||||
console.warn(e);
|
||||
|
||||
// Add a result anyways, just without a profile. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: term,
|
||||
avatar_url: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
serverResultsMixin: r.results.map(u => ({
|
||||
userId: u.user_id,
|
||||
user: new DirectoryMember(u),
|
||||
})),
|
||||
});
|
||||
}).catch(e => {
|
||||
console.error("Error searching user directory:");
|
||||
console.error(e);
|
||||
this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
|
||||
});
|
||||
|
||||
// Whenever we search the directory, also try to search the identity server. It's
|
||||
// all debounced the same anyways.
|
||||
if (!this.state.canUseIdentityServer) {
|
||||
// The user doesn't have an identity server set - warn them of that.
|
||||
this.setState({tryingIdentityServer: true});
|
||||
return;
|
||||
}
|
||||
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||
// Start off by suggesting the plain email while we try and resolve it
|
||||
// to a real account.
|
||||
this.setState({
|
||||
// per above: the userId is a lie here - it's just a regular identifier
|
||||
threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}],
|
||||
});
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const token = await authClient.getAccessToken();
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||
'email',
|
||||
term,
|
||||
undefined, // callback
|
||||
token,
|
||||
);
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
if (!lookup || !lookup.mxid) {
|
||||
// We weren't able to find anyone - we're already suggesting the plain email
|
||||
// as an alternative, so do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
// We append the user suggestion to give the user an option to click
|
||||
// the email anyways, and so we don't cause things to jump around. In
|
||||
// theory, the user would see the user pop up and think "ah yes, that
|
||||
// person!"
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||
this.setState({
|
||||
threepidResultsMixin: [...this.state.threepidResultsMixin, {
|
||||
user: new DirectoryMember({
|
||||
user_id: lookup.mxid,
|
||||
display_name: profile.displayname,
|
||||
avatar_url: profile.avatar_url,
|
||||
}),
|
||||
userId: lookup.mxid,
|
||||
}],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error searching identity server:");
|
||||
console.error(e);
|
||||
this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
const term = e.target.value;
|
||||
this.setState({filterText: term});
|
||||
@@ -697,113 +822,8 @@ export default class InviteDialog extends React.PureComponent {
|
||||
if (this._debounceTimer) {
|
||||
clearTimeout(this._debounceTimer);
|
||||
}
|
||||
this._debounceTimer = setTimeout(async () => {
|
||||
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||
if (term !== this.state.filterText) {
|
||||
// Discard the results - we were probably too slow on the server-side to make
|
||||
// these results useful. This is a race we want to avoid because we could overwrite
|
||||
// more accurate results.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!r.results) r.results = [];
|
||||
|
||||
// While we're here, try and autocomplete a search result for the mxid itself
|
||||
// if there's no matches (and the input looks like a mxid).
|
||||
if (term[0] === '@' && term.indexOf(':') > 1) {
|
||||
try {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
||||
if (profile) {
|
||||
// If we have a profile, we have enough information to assume that
|
||||
// the mxid can be invited - add it to the list. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: profile['displayname'],
|
||||
avatar_url: profile['avatar_url'],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Non-fatal error trying to make an invite for a user ID");
|
||||
console.warn(e);
|
||||
|
||||
// Add a result anyways, just without a profile. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: term,
|
||||
avatar_url: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
serverResultsMixin: r.results.map(u => ({
|
||||
userId: u.user_id,
|
||||
user: new DirectoryMember(u),
|
||||
})),
|
||||
});
|
||||
}).catch(e => {
|
||||
console.error("Error searching user directory:");
|
||||
console.error(e);
|
||||
this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
|
||||
});
|
||||
|
||||
// Whenever we search the directory, also try to search the identity server. It's
|
||||
// all debounced the same anyways.
|
||||
if (!this.state.canUseIdentityServer) {
|
||||
// The user doesn't have an identity server set - warn them of that.
|
||||
this.setState({tryingIdentityServer: true});
|
||||
return;
|
||||
}
|
||||
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||
// Start off by suggesting the plain email while we try and resolve it
|
||||
// to a real account.
|
||||
this.setState({
|
||||
// per above: the userId is a lie here - it's just a regular identifier
|
||||
threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}],
|
||||
});
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const token = await authClient.getAccessToken();
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||
'email',
|
||||
term,
|
||||
undefined, // callback
|
||||
token,
|
||||
);
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
if (!lookup || !lookup.mxid) {
|
||||
// We weren't able to find anyone - we're already suggesting the plain email
|
||||
// as an alternative, so do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
// We append the user suggestion to give the user an option to click
|
||||
// the email anyways, and so we don't cause things to jump around. In
|
||||
// theory, the user would see the user pop up and think "ah yes, that
|
||||
// person!"
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||
this.setState({
|
||||
threepidResultsMixin: [...this.state.threepidResultsMixin, {
|
||||
user: new DirectoryMember({
|
||||
user_id: lookup.mxid,
|
||||
display_name: profile.displayname,
|
||||
avatar_url: profile.avatar_url,
|
||||
}),
|
||||
userId: lookup.mxid,
|
||||
}],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error searching identity server:");
|
||||
console.error(e);
|
||||
this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
|
||||
}
|
||||
}
|
||||
this._debounceTimer = setTimeout(() => {
|
||||
this._updateSuggestions(term);
|
||||
}, 150); // 150ms debounce (human reaction time + some)
|
||||
};
|
||||
|
||||
|
||||
@@ -31,12 +31,14 @@ import {
|
||||
ModalButtonKind,
|
||||
Widget,
|
||||
WidgetApiFromWidgetAction,
|
||||
WidgetKind,
|
||||
} from "matrix-widget-api";
|
||||
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
@@ -63,7 +65,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.widget = new Widget({
|
||||
this.widget = new ElementWidget({
|
||||
...this.props.widgetDefinition,
|
||||
creatorUserId: MatrixClientPeg.get().getUserId(),
|
||||
id: `modal_${this.props.sourceWidgetId}`,
|
||||
@@ -72,7 +74,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const driver = new StopGapWidgetDriver( []);
|
||||
const driver = new StopGapWidgetDriver( [], this.widget, WidgetKind.Modal);
|
||||
const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
|
||||
this.setState({messaging});
|
||||
}
|
||||
@@ -160,7 +162,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
this.state.messaging.notifyModalWidgetButtonClicked(def.id);
|
||||
};
|
||||
|
||||
return <AccessibleButton key={def.id} kind={kind} onClick={onClick}>
|
||||
const isDisabled = this.state.disabledButtonIds.includes(def.id);
|
||||
|
||||
return <AccessibleButton key={def.id} kind={kind} onClick={onClick} disabled={isDisabled}>
|
||||
{ def.label }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import {useRef, useState} from "react";
|
||||
import Field from "../elements/Field";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import withValidation from "../elements/Validation";
|
||||
import * as Email from "../../../email";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
onFinished(continued: boolean, email?: string): void;
|
||||
}
|
||||
|
||||
const validation = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const fieldRef = useRef<Field>();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (email) {
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!valid) {
|
||||
fieldRef.current.focus();
|
||||
fieldRef.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onFinished(true, email);
|
||||
};
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Continuing without email")}
|
||||
className="mx_RegistrationEmailPromptDialog"
|
||||
contentId="mx_RegistrationEmailPromptDialog"
|
||||
onFinished={() => onFinished(false)}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_Dialog_content" id="mx_RegistrationEmailPromptDialog">
|
||||
<p>{_t("Just a heads up, if you don't add an email and forget your password, you could " +
|
||||
"<b>permanently lose access to your account</b>.", {}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
})}</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Field
|
||||
ref={fieldRef}
|
||||
type="text"
|
||||
label={_t("Email (optional)")}
|
||||
value={email}
|
||||
onChange={ev => {
|
||||
setEmail(ev.target.value);
|
||||
}}
|
||||
onValidate={async fieldState => await validation(fieldState)}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={onSubmit}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default RegistrationEmailPromptDialog;
|
||||
@@ -53,9 +53,9 @@ export default class RoomSettingsDialog extends React.Component {
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
// When room changes below us, close the room settings
|
||||
// When view changes below us, close the room settings
|
||||
// whilst the modal is open this can only be triggered when someone hits Leave Room
|
||||
if (payload.action === 'view_next_room') {
|
||||
if (payload.action === 'view_home_page') {
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 it’s a good place for many.");
|
||||
}
|
||||
|
||||
let defaultServerName = this.defaultServer.hsName;
|
||||
if (this.defaultServer.hsNameIsDifferent) {
|
||||
defaultServerName = (
|
||||
<TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>
|
||||
{this.defaultServer.hsName}
|
||||
</TextWithTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={this.props.title || _t("Sign into your homeserver")}
|
||||
className="mx_ServerPickerDialog"
|
||||
contentId="mx_ServerPickerDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
fixedWidth={false}
|
||||
hasCancel={true}
|
||||
>
|
||||
<form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
|
||||
<p>
|
||||
{_t("We call the places 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>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
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 BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import {
|
||||
Capability,
|
||||
Widget,
|
||||
WidgetEventCapability,
|
||||
WidgetKind,
|
||||
} from "matrix-widget-api";
|
||||
import { objectShallowClone } from "../../../utils/objects";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import { CapabilityText } from "../../../widgets/CapabilityText";
|
||||
|
||||
export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
|
||||
return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
|
||||
}
|
||||
|
||||
function setRememberedCapabilitiesForWidget(widget: Widget, caps: Capability[]) {
|
||||
localStorage.setItem(`widget_${widget.id}_approved_caps`, JSON.stringify(caps));
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
requestedCapabilities: Set<Capability>;
|
||||
widget: Widget;
|
||||
widgetKind: WidgetKind; // TODO: Refactor into the Widget class
|
||||
}
|
||||
|
||||
interface IBooleanStates {
|
||||
// @ts-ignore - TS wants a string key, but we know better
|
||||
[capability: Capability]: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
booleanStates: IBooleanStates;
|
||||
rememberSelection: boolean;
|
||||
}
|
||||
|
||||
export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<IProps, IState> {
|
||||
private eventPermissionsMap = new Map<Capability, WidgetEventCapability>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const parsedEvents = WidgetEventCapability.findEventCapabilities(this.props.requestedCapabilities);
|
||||
parsedEvents.forEach(e => this.eventPermissionsMap.set(e.raw, e));
|
||||
|
||||
const states: IBooleanStates = {};
|
||||
this.props.requestedCapabilities.forEach(c => states[c] = true);
|
||||
|
||||
this.state = {
|
||||
booleanStates: states,
|
||||
rememberSelection: true,
|
||||
};
|
||||
}
|
||||
|
||||
private onToggle = (capability: Capability) => {
|
||||
const newStates = objectShallowClone(this.state.booleanStates);
|
||||
newStates[capability] = !newStates[capability];
|
||||
this.setState({booleanStates: newStates});
|
||||
};
|
||||
|
||||
private onRememberSelectionChange = (newVal: boolean) => {
|
||||
this.setState({rememberSelection: newVal});
|
||||
};
|
||||
|
||||
private onSubmit = async (ev) => {
|
||||
this.closeAndTryRemember(Object.entries(this.state.booleanStates)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([cap]) => cap));
|
||||
};
|
||||
|
||||
private onReject = async (ev) => {
|
||||
this.closeAndTryRemember([]); // nothing was approved
|
||||
};
|
||||
|
||||
private closeAndTryRemember(approved: Capability[]) {
|
||||
if (this.state.rememberSelection) {
|
||||
setRememberedCapabilitiesForWidget(this.props.widget, approved);
|
||||
}
|
||||
this.props.onFinished({approved});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => {
|
||||
const text = CapabilityText.for(cap, this.props.widgetKind);
|
||||
const byline = text.byline
|
||||
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{text.byline}</span>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx_WidgetCapabilitiesPromptDialog_cap" key={cap + i}>
|
||||
<StyledCheckbox
|
||||
checked={isChecked}
|
||||
onChange={() => this.onToggle(cap)}
|
||||
>{text.primary}</StyledCheckbox>
|
||||
{byline}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_WidgetCapabilitiesPromptDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Approve widget permissions")}
|
||||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="text-muted">{_t("This widget would like to:")}</div>
|
||||
{checkboxRows}
|
||||
<DialogButtons
|
||||
primaryButton={_t("Approve")}
|
||||
cancelButton={_t("Decline All")}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
onCancel={this.onReject}
|
||||
additive={
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.rememberSelection}
|
||||
toggleInFront={true}
|
||||
onChange={this.onRememberSelectionChange}
|
||||
label={_t("Remember my selection for this widget")} />}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,17 @@ limitations under the License.
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import * as sdk from "../../../index";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import {Widget} from "matrix-widget-api";
|
||||
import {OIDCState, WidgetPermissionStore} from "../../../stores/widgets/WidgetPermissionStore";
|
||||
|
||||
export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
widgetUrl: PropTypes.string.isRequired,
|
||||
widgetId: PropTypes.string.isRequired,
|
||||
isUserWidget: PropTypes.bool.isRequired,
|
||||
widget: PropTypes.objectOf(Widget).isRequired,
|
||||
widgetKind: PropTypes.string.isRequired, // WidgetKind from widget-api
|
||||
inRoomId: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@@ -51,16 +50,10 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||
if (this.state.rememberSelection) {
|
||||
console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
|
||||
|
||||
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
if (!currentValues.allow) currentValues.allow = [];
|
||||
if (!currentValues.deny) currentValues.deny = [];
|
||||
|
||||
const securityKey = WidgetUtils.getWidgetSecurityKey(
|
||||
this.props.widgetId,
|
||||
this.props.widgetUrl,
|
||||
this.props.isUserWidget);
|
||||
(allowed ? currentValues.allow : currentValues.deny).push(securityKey);
|
||||
SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
|
||||
WidgetPermissionStore.instance.setOIDCState(
|
||||
this.props.widget, this.props.widgetKind, this.props.inRoomId,
|
||||
allowed ? OIDCState.Allowed : OIDCState.Denied,
|
||||
);
|
||||
}
|
||||
|
||||
this.props.onFinished(allowed);
|
||||
@@ -84,7 +77,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||
"A widget located at %(widgetUrl)s would like to verify your identity. " +
|
||||
"By allowing this, the widget will be able to verify your user ID, but not " +
|
||||
"perform actions as you.", {
|
||||
widgetUrl: this.props.widgetUrl.split("?")[0],
|
||||
widgetUrl: this.props.widget.templateUrl.split("?")[0],
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -23,7 +23,6 @@ import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import Spinner from './Spinner';
|
||||
@@ -375,11 +374,13 @@ export default class AppTile extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
// if the widget would be allowed to remain on screen, we must put it in
|
||||
// a PersistedElement from the get-go, otherwise the iframe will be
|
||||
// re-mounted later when we do.
|
||||
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
|
||||
if (!this.props.userWidget) {
|
||||
// All room widgets can theoretically be allowed to remain on screen, so we
|
||||
// wrap them all in a PersistedElement from the get-go. If we wait, the iframe
|
||||
// will be re-mounted later, which means the widget has to start over, which is
|
||||
// bad.
|
||||
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
@@ -474,10 +475,6 @@ AppTile.propTypes = {
|
||||
handleMinimisePointerEvents: PropTypes.bool,
|
||||
// Optionally hide the popout widget icon
|
||||
showPopout: PropTypes.bool,
|
||||
// Widget capabilities to allow by default (without user confirmation)
|
||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
whitelistCapabilities: PropTypes.array,
|
||||
// Is this an instance of a user widget
|
||||
userWidget: PropTypes.bool,
|
||||
};
|
||||
@@ -488,7 +485,6 @@ AppTile.defaultProps = {
|
||||
showTitle: true,
|
||||
showPopout: true,
|
||||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
userWidget: false,
|
||||
miniMode: false,
|
||||
};
|
||||
|
||||
@@ -54,6 +54,9 @@ export default class DialogButtons extends React.Component {
|
||||
|
||||
// disables only the primary button
|
||||
primaryDisabled: PropTypes.bool,
|
||||
|
||||
// something to stick next to the buttons, optionally
|
||||
additive: PropTypes.element,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -85,8 +88,14 @@ export default class DialogButtons extends React.Component {
|
||||
</button>;
|
||||
}
|
||||
|
||||
let additive = null;
|
||||
if (this.props.additive) {
|
||||
additive = <div className="mx_Dialog_buttons_additive">{this.props.additive}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_Dialog_buttons">
|
||||
{ additive }
|
||||
{ cancelButton }
|
||||
{ this.props.children }
|
||||
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'}
|
||||
|
||||
@@ -20,8 +20,8 @@ import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class DirectorySearchBox extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._collectInput = this._collectInput.bind(this);
|
||||
this._onClearClick = this._onClearClick.bind(this);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
@@ -31,7 +31,7 @@ export default class DirectorySearchBox extends React.Component {
|
||||
this.input = null;
|
||||
|
||||
this.state = {
|
||||
value: '',
|
||||
value: this.props.initialText || '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,15 +90,20 @@ export default class DirectorySearchBox extends React.Component {
|
||||
}
|
||||
|
||||
return <div className={`mx_DirectorySearchBox ${this.props.className} mx_textinput`}>
|
||||
<input type="text" name="dirsearch" value={this.state.value}
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
ref={this._collectInput}
|
||||
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
||||
placeholder={this.props.placeholder} autoFocus
|
||||
/>
|
||||
{ joinButton }
|
||||
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick}></AccessibleButton>
|
||||
</div>;
|
||||
<input
|
||||
type="text"
|
||||
name="dirsearch"
|
||||
value={this.state.value}
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
ref={this._collectInput}
|
||||
onChange={this._onChange}
|
||||
onKeyUp={this._onKeyUp}
|
||||
placeholder={this.props.placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
{ joinButton }
|
||||
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,4 +114,5 @@ DirectorySearchBox.propTypes = {
|
||||
onJoinClick: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
showJoinButton: PropTypes.bool,
|
||||
initialText: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -61,10 +61,14 @@ 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>.
|
||||
}
|
||||
|
||||
interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||
export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||
// The element to create. Defaults to "input".
|
||||
element?: "input";
|
||||
// The input's value. This is a controlled component, so the value is required.
|
||||
@@ -100,6 +104,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
public static readonly defaultProps = {
|
||||
element: "input",
|
||||
type: "text",
|
||||
validateOnFocus: true,
|
||||
validateOnBlur: true,
|
||||
validateOnChange: true,
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -137,9 +144,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
this.setState({
|
||||
focused: true,
|
||||
});
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
if (this.props.validateOnFocus) {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}
|
||||
// Parent component may have supplied its own `onFocus` as well
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(ev);
|
||||
@@ -147,7 +156,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
};
|
||||
|
||||
private onChange = (ev) => {
|
||||
this.validateOnChange();
|
||||
if (this.props.validateOnChange) {
|
||||
this.validateOnChange();
|
||||
}
|
||||
// Parent component may have supplied its own `onChange` as well
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(ev);
|
||||
@@ -158,16 +169,18 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
this.setState({
|
||||
focused: false,
|
||||
});
|
||||
this.validate({
|
||||
focused: false,
|
||||
});
|
||||
if (this.props.validateOnBlur) {
|
||||
this.validate({
|
||||
focused: false,
|
||||
});
|
||||
}
|
||||
// Parent component may have supplied its own `onBlur` as well
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(ev);
|
||||
}
|
||||
};
|
||||
|
||||
private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
|
||||
public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
@@ -196,12 +209,15 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
feedbackVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
public render() {
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { element, prefixComponent, postfixComponent, className, onValidate, children,
|
||||
tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props;
|
||||
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
|
||||
...inputProps} = this.props;
|
||||
|
||||
// Set some defaults for the <input> element
|
||||
const ref = input => this.input = input;
|
||||
|
||||
@@ -21,6 +21,8 @@ import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip from './Tooltip';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useTimeout} from "../../../hooks/useTimeout";
|
||||
import Analytics from "../../../Analytics";
|
||||
import CountlyAnalytics from '../../../CountlyAnalytics';
|
||||
|
||||
export const AVATAR_SIZE = 52;
|
||||
|
||||
@@ -56,6 +58,8 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
||||
onChange={async (ev) => {
|
||||
if (!ev.target.files?.length) return;
|
||||
setBusy(true);
|
||||
Analytics.trackEvent("mini_avatar", "upload");
|
||||
CountlyAnalytics.instance.track("mini_avatar_upload");
|
||||
const file = ev.target.files[0];
|
||||
const uri = await cli.uploadContent(file);
|
||||
await setAvatarUrl(uri);
|
||||
|
||||
@@ -71,7 +71,6 @@ export default class PersistentApp extends React.Component {
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
||||
persistentWidgetInRoomId, appEvent.getId(),
|
||||
);
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
|
||||
const AppTile = sdk.getComponent('elements.AppTile');
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
@@ -82,7 +81,6 @@ export default class PersistentApp extends React.Component {
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
miniMode={true}
|
||||
showMenubar={false}
|
||||
/>;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -32,7 +32,7 @@ interface IRule<T, D = void> {
|
||||
|
||||
interface IArgs<T, D = void> {
|
||||
rules: IRule<T, D>[];
|
||||
description(this: T, derivedData: D): React.ReactChild;
|
||||
description?(this: T, derivedData: D): React.ReactChild;
|
||||
hideDescriptionIfValid?: boolean;
|
||||
deriveData?(data: Data): Promise<D>;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ interface IState {
|
||||
}
|
||||
|
||||
export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
private videoRef = React.createRef<HTMLVideoElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -71,7 +73,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
_getContentUrl(): string|null {
|
||||
private getContentUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedUrl;
|
||||
@@ -80,7 +82,12 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
_getThumbUrl(): string|null {
|
||||
private hasContentUrl(): boolean {
|
||||
const url = this.getContentUrl();
|
||||
return url && !url.startsWith("data:");
|
||||
}
|
||||
|
||||
private getThumbUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
@@ -118,7 +125,10 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
} else {
|
||||
console.log("NOT preloading video");
|
||||
this.setState({
|
||||
decryptedUrl: null,
|
||||
// For Chrome and Electron, we need to set some non-empty `src` to
|
||||
// enable the play button. Firefox does not seem to care either
|
||||
// way, so it's fine to do for all browsers.
|
||||
decryptedUrl: `data:${content?.info?.mimetype},`,
|
||||
decryptedThumbnailUrl: thumbnailUrl,
|
||||
decryptedBlob: null,
|
||||
});
|
||||
@@ -142,8 +152,8 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
async _videoOnPlay() {
|
||||
if (this._getContentUrl() || this.state.fetchingData || this.state.error) {
|
||||
private videoOnPlay = async () => {
|
||||
if (this.hasContentUrl() || this.state.fetchingData || this.state.error) {
|
||||
// We have the file, we are fetching the file, or there is an error.
|
||||
return;
|
||||
}
|
||||
@@ -164,6 +174,9 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
decryptedUrl: contentUrl,
|
||||
decryptedBlob: decryptedBlob,
|
||||
fetchingData: false,
|
||||
}, () => {
|
||||
if (!this.videoRef.current) return;
|
||||
this.videoRef.current.play();
|
||||
});
|
||||
this.props.onHeightChanged();
|
||||
}
|
||||
@@ -195,8 +208,8 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
);
|
||||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
const thumbUrl = this._getThumbUrl();
|
||||
const contentUrl = this.getContentUrl();
|
||||
const thumbUrl = this.getThumbUrl();
|
||||
let height = null;
|
||||
let width = null;
|
||||
let poster = null;
|
||||
@@ -215,9 +228,20 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
return (
|
||||
<span className="mx_MVideoBody">
|
||||
<video className="mx_MVideoBody" src={contentUrl} title={content.body}
|
||||
controls preload={preload} muted={autoplay} autoPlay={autoplay}
|
||||
height={height} width={width} poster={poster} onPlay={this._videoOnPlay.bind(this)}>
|
||||
<video
|
||||
className="mx_MVideoBody"
|
||||
ref={this.videoRef}
|
||||
src={contentUrl}
|
||||
title={content.body}
|
||||
controls
|
||||
preload={preload}
|
||||
muted={autoplay}
|
||||
autoPlay={autoplay}
|
||||
height={height}
|
||||
width={width}
|
||||
poster={poster}
|
||||
onPlay={this.videoOnPlay}
|
||||
>
|
||||
</video>
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
|
||||
@@ -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") }
|
||||
|
||||
@@ -28,7 +28,7 @@ import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import createRoom, {privateShouldBeEncrypted} from '../../../createRoom';
|
||||
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
@@ -105,17 +105,7 @@ export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice
|
||||
};
|
||||
|
||||
async function openDMForUser(matrixClient: MatrixClient, userId: string) {
|
||||
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
if (!room || room.getMyMembership() === "leave") {
|
||||
return lastActiveRoom;
|
||||
}
|
||||
if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) {
|
||||
return room;
|
||||
}
|
||||
return lastActiveRoom;
|
||||
}, null);
|
||||
const lastActiveRoom = findDMForUser(matrixClient, userId);
|
||||
|
||||
if (lastActiveRoom) {
|
||||
dis.dispatch({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -103,7 +103,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, room.roomId)}
|
||||
/>
|
||||
</BaseCard>;
|
||||
};
|
||||
|
||||
@@ -210,8 +210,6 @@ export default class AppsDrawer extends React.Component {
|
||||
if (!this.props.showApps) return <div />;
|
||||
|
||||
const apps = this.state.apps.map((app, index, arr) => {
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
||||
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
app={app}
|
||||
@@ -221,7 +219,6 @@ export default class AppsDrawer extends React.Component {
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
/>);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,9 +29,10 @@ import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import classNames from 'classnames';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
@@ -136,7 +137,10 @@ export default class EditMessageComposer extends React.Component {
|
||||
if (event.metaKey || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
if (event.key === Key.ENTER) {
|
||||
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
|
||||
const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
|
||||
: event.key === Key.ENTER;
|
||||
if (send) {
|
||||
this._sendEdit();
|
||||
event.preventDefault();
|
||||
} else if (event.key === Key.ESCAPE) {
|
||||
|
||||
@@ -745,13 +745,22 @@ export default class EventTile extends React.Component {
|
||||
}
|
||||
|
||||
if (this.props.mxEvent.sender && avatarSize) {
|
||||
let member;
|
||||
// set member to receiver (target) if it is a 3PID invite
|
||||
// so that the correct avatar is shown as the text is
|
||||
// `$target accepted the invitation for $email`
|
||||
if (this.props.mxEvent.getContent().third_party_invite) {
|
||||
member = this.props.mxEvent.target;
|
||||
} else {
|
||||
member = this.props.mxEvent.sender;
|
||||
}
|
||||
avatar = (
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={this.props.mxEvent.sender}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={member}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
sublists: ITagMap;
|
||||
isNameFiltering: boolean;
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
@@ -183,6 +184,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
|
||||
this.state = {
|
||||
sublists: {},
|
||||
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
@@ -253,7 +255,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
return CustomRoomTagStore.getTags()[t];
|
||||
});
|
||||
|
||||
let doUpdate = arrayHasDiff(previousListIds, newListIds);
|
||||
const isNameFiltering = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||
let doUpdate = this.state.isNameFiltering !== isNameFiltering || arrayHasDiff(previousListIds, newListIds);
|
||||
if (!doUpdate) {
|
||||
// so we didn't have the visible sublists change, but did the contents of those
|
||||
// sublists change significantly enough to break the sticky headers? Probably, so
|
||||
@@ -275,14 +278,20 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
const newSublists = objectWithOnly(newLists, newListIds);
|
||||
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
||||
|
||||
this.setState({sublists}, () => {
|
||||
this.setState({sublists, isNameFiltering}, () => {
|
||||
this.props.onResize();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onStartChat = () => {
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
dis.dispatch({ action: "view_create_chat", initialText });
|
||||
};
|
||||
|
||||
private onExplore = () => {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||
};
|
||||
|
||||
private renderCommunityInvites(): TemporaryTile[] {
|
||||
@@ -332,8 +341,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
return p;
|
||||
}, [] as TagID[]);
|
||||
|
||||
// show a skeleton UI if the user is in no rooms
|
||||
const showSkeleton = Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
||||
// show a skeleton UI if the user is in no rooms and they are not filtering
|
||||
const showSkeleton = !this.state.isNameFiltering &&
|
||||
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
||||
|
||||
for (const orderedTagId of tagOrder) {
|
||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||
@@ -370,10 +380,21 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
public render() {
|
||||
let explorePrompt: JSX.Element;
|
||||
if (!this.props.isMinimized) {
|
||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||
if (this.state.isNameFiltering) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Can't see what you’re looking for?")}</div>
|
||||
<AccessibleButton kind="link" onClick={this.onExplore}>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_startChat"
|
||||
kind="link"
|
||||
onClick={this.onStartChat}
|
||||
>
|
||||
{_t("Start a new chat")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_explore"
|
||||
kind="link"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore all public rooms")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
@@ -385,7 +406,18 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
|
||||
<AccessibleButton kind="link" onClick={this.onExplore}>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_startChat"
|
||||
kind="link"
|
||||
onClick={this.onStartChat}
|
||||
>
|
||||
{_t("Start a new chat")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_explore"
|
||||
kind="link"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore all public rooms")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
|
||||
@@ -422,7 +422,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
room = this.state.rooms && this.state.rooms[0];
|
||||
} else {
|
||||
// find the first room with a count of the same colour as the badge count
|
||||
room = this.state.rooms.find((r: Room) => {
|
||||
room = RoomListStore.instance.unfilteredLists[this.props.tagId].find((r: Room) => {
|
||||
const notifState = this.notificationState.getForRoom(r);
|
||||
return notifState.count > 0 && notifState.color === this.notificationState.color;
|
||||
});
|
||||
|
||||
@@ -38,10 +38,11 @@ import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import EMOJI_REGEX from 'emojibase-regex';
|
||||
@@ -142,7 +143,11 @@ export default class SendMessageComposer extends React.Component {
|
||||
return;
|
||||
}
|
||||
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
if (event.key === Key.ENTER && !hasModifier) {
|
||||
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
|
||||
const send = ctrlEnterToSend
|
||||
? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
|
||||
: event.key === Key.ENTER && !hasModifier;
|
||||
if (send) {
|
||||
this._sendMessage();
|
||||
event.preventDefault();
|
||||
} else if (event.key === Key.ARROW_UP) {
|
||||
|
||||
@@ -280,7 +280,6 @@ export default class Stickerpicker extends React.Component {
|
||||
showPopout={false}
|
||||
onMinimiseClick={this._onHideStickersClick}
|
||||
handleMinimisePointerEvents={true}
|
||||
whitelistCapabilities={['m.sticker', 'visibility']}
|
||||
userWidget={true}
|
||||
/>
|
||||
</PersistedElement>
|
||||
|
||||
@@ -21,9 +21,18 @@ import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import withValidation from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import PassphraseField from "../auth/PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
const FIELD_OLD_PASSWORD = 'field_old_password';
|
||||
const FIELD_NEW_PASSWORD = 'field_new_password';
|
||||
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
||||
|
||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||
|
||||
export default class ChangePassword extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -63,6 +72,7 @@ export default class ChangePassword extends React.Component {
|
||||
}
|
||||
|
||||
state = {
|
||||
fieldValid: {},
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
@@ -168,26 +178,84 @@ export default class ChangePassword extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
markFieldValid(fieldID, valid) {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
fieldValid,
|
||||
});
|
||||
}
|
||||
|
||||
onChangeOldPassword = (ev) => {
|
||||
this.setState({
|
||||
oldPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onOldPasswordValidate = async fieldState => {
|
||||
const result = await this.validateOldPasswordRules(fieldState);
|
||||
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validateOldPasswordRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Passwords can't be empty"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onChangeNewPassword = (ev) => {
|
||||
this.setState({
|
||||
newPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onNewPasswordValidate = result => {
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||
};
|
||||
|
||||
onChangeNewPasswordConfirm = (ev) => {
|
||||
this.setState({
|
||||
newPasswordConfirm: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onClickChange = (ev) => {
|
||||
onNewPasswordConfirmValidate = async fieldState => {
|
||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validatePasswordConfirmRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Confirm password"),
|
||||
},
|
||||
{
|
||||
key: "match",
|
||||
test({ value }) {
|
||||
return !value || value === this.state.newPassword;
|
||||
},
|
||||
invalid: () => _t("Passwords don't match"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onClickChange = async (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPassword = this.state.oldPassword;
|
||||
const newPassword = this.state.newPassword;
|
||||
const confirmPassword = this.state.newPasswordConfirm;
|
||||
@@ -201,9 +269,75 @@ export default class ChangePassword extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
// TODO: Live validation on `new pw == confirm pw`
|
||||
async verifyFieldsBeforeSubmit() {
|
||||
// Blur the active element if any, so we first run its blur validation,
|
||||
// which is less strict than the pass we're about to do below for all fields.
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
||||
const fieldIDsInDisplayOrder = [
|
||||
FIELD_OLD_PASSWORD,
|
||||
FIELD_NEW_PASSWORD,
|
||||
FIELD_NEW_PASSWORD_CONFIRM,
|
||||
];
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
// values for required fields.
|
||||
for (const fieldID of fieldIDsInDisplayOrder) {
|
||||
const field = this[fieldID];
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
// We must wait for these validations to finish before queueing
|
||||
// up the setState below so our setState goes in the queue after
|
||||
// all the setStates from these validate calls (that's how we
|
||||
// know they've finished).
|
||||
await field.validate({ allowEmpty: false });
|
||||
}
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
|
||||
|
||||
if (!invalidField) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Focus the first invalid field and show feedback in the stricter mode
|
||||
// that no longer allows empty values for required fields.
|
||||
invalidField.focus();
|
||||
invalidField.validate({ allowEmpty: false, focused: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
allFieldsValid() {
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (!this.state.fieldValid[keys[i]]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
findFirstInvalidField(fieldIDs) {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const rowClassName = this.props.rowClassName;
|
||||
const buttonClassName = this.props.buttonClassName;
|
||||
|
||||
@@ -213,28 +347,35 @@ export default class ChangePassword extends React.Component {
|
||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={field => this[FIELD_OLD_PASSWORD] = field}
|
||||
type="password"
|
||||
label={_t('Current password')}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
onValidate={this.onOldPasswordValidate}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
<PassphraseField
|
||||
fieldRef={field => this[FIELD_NEW_PASSWORD] = field}
|
||||
type="password"
|
||||
label={_t('New Password')}
|
||||
label='New Password'
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.newPassword}
|
||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||
onChange={this.onChangeNewPassword}
|
||||
onValidate={this.onNewPasswordValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={field => this[FIELD_NEW_PASSWORD_CONFIRM] = field}
|
||||
type="password"
|
||||
label={_t("Confirm password")}
|
||||
value={this.state.newPasswordConfirm}
|
||||
onChange={this.onChangeNewPasswordConfirm}
|
||||
onValidate={this.onNewPasswordConfirmValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -130,10 +130,13 @@ export default class EventIndexPanel extends React.Component {
|
||||
<div>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t("Securely cache encrypted messages locally for them " +
|
||||
"to appear in search results, using %(size)s to store messages from %(count)s rooms.",
|
||||
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
|
||||
{
|
||||
size: formatBytes(this.state.eventIndexSize, 0),
|
||||
count: formatCountLong(this.state.roomCount),
|
||||
// This drives the singular / plural string
|
||||
// selection for "room" / "rooms" only.
|
||||
count: this.state.roomCount,
|
||||
rooms: formatCountLong(this.state.roomCount),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,6 +33,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||
'MessageComposerInput.autoReplaceEmoji',
|
||||
'MessageComposerInput.suggestEmoji',
|
||||
'sendTypingNotifications',
|
||||
'MessageComposerInput.ctrlEnterToSend',
|
||||
];
|
||||
|
||||
static TIMELINE_SETTINGS = [
|
||||
|
||||
@@ -26,6 +26,15 @@ import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
CallState.InviteSent,
|
||||
CallState.Connecting,
|
||||
CallState.CreateAnswer,
|
||||
CallState.CreateOffer,
|
||||
CallState.WaitLocalMedia,
|
||||
];
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
@@ -94,14 +103,13 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
|
||||
const showCall = (
|
||||
this.state.activeCall &&
|
||||
this.state.activeCall.state === CallState.Connected &&
|
||||
SHOW_CALL_IN_STATES.includes(this.state.activeCall.state) &&
|
||||
!callForRoom
|
||||
);
|
||||
|
||||
if (showCall) {
|
||||
return (
|
||||
<CallView
|
||||
className="mx_CallPreview"
|
||||
onClick={this.onCallViewClick}
|
||||
showHangup={true}
|
||||
/>
|
||||
|
||||
@@ -21,12 +21,13 @@ import dis from '../../../dispatcher/dispatcher';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import VideoFeed, { VideoFeedType } from "./VideoFeed";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
||||
|
||||
interface IProps {
|
||||
// js-sdk room object. If set, we will only show calls for the given
|
||||
@@ -43,9 +44,6 @@ interface IProps {
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
|
||||
// classname applied to view,
|
||||
className?: string;
|
||||
|
||||
// Whether to show the hang up icon:W
|
||||
showHangup?: boolean;
|
||||
}
|
||||
@@ -53,6 +51,10 @@ interface IProps {
|
||||
interface IState {
|
||||
call: MatrixCall;
|
||||
isLocalOnHold: boolean,
|
||||
micMuted: boolean,
|
||||
vidMuted: boolean,
|
||||
callState: CallState,
|
||||
controlsVisible: boolean,
|
||||
}
|
||||
|
||||
function getFullScreenElement() {
|
||||
@@ -83,10 +85,15 @@ function exitFullscreen() {
|
||||
if (exitMethod) exitMethod.call(document);
|
||||
}
|
||||
|
||||
const CONTROLS_HIDE_DELAY = 1000;
|
||||
// Height of the header duplicated from CSS because we need to subtract it from our max
|
||||
// height to get the max height of the video
|
||||
const HEADER_HEIGHT = 44;
|
||||
|
||||
export default class CallView extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private container = createRef<HTMLDivElement>();
|
||||
|
||||
private contentRef = createRef<HTMLDivElement>();
|
||||
private controlsHideTimer: number = null;
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
@@ -94,6 +101,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
this.state = {
|
||||
call,
|
||||
isLocalOnHold: call ? call.isLocalOnHold() : null,
|
||||
micMuted: call ? call.isMicrophoneMuted() : null,
|
||||
vidMuted: call ? call.isLocalVideoMuted() : null,
|
||||
callState: call ? call.state : null,
|
||||
controlsVisible: true,
|
||||
}
|
||||
|
||||
this.updateCallListeners(null, call);
|
||||
@@ -101,9 +112,11 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
document.addEventListener('keydown', this.onNativeKeyDown);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener("keydown", this.onNativeKeyDown);
|
||||
this.updateCallListeners(this.state.call, null);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
@@ -111,11 +124,11 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
private onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case 'video_fullscreen': {
|
||||
if (!this.container.current) {
|
||||
if (!this.contentRef.current) {
|
||||
return;
|
||||
}
|
||||
if (payload.fullscreen) {
|
||||
requestFullscreen(this.container.current);
|
||||
requestFullscreen(this.contentRef.current);
|
||||
} else if (getFullScreenElement()) {
|
||||
exitFullscreen();
|
||||
}
|
||||
@@ -125,9 +138,21 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
const newCall = this.getCall();
|
||||
if (newCall !== this.state.call) {
|
||||
this.updateCallListeners(this.state.call, newCall);
|
||||
let newControlsVisible = this.state.controlsVisible;
|
||||
if (newCall && !this.state.call) {
|
||||
newControlsVisible = true;
|
||||
if (this.controlsHideTimer !== null) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
}
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
this.setState({
|
||||
call: newCall,
|
||||
isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
|
||||
micMuted: newCall ? newCall.isMicrophoneMuted() : null,
|
||||
vidMuted: newCall ? newCall.isLocalVideoMuted() : null,
|
||||
callState: newCall ? newCall.state : null,
|
||||
controlsVisible: newControlsVisible,
|
||||
});
|
||||
}
|
||||
if (!newCall && getFullScreenElement()) {
|
||||
@@ -144,11 +169,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
if (this.props.room) {
|
||||
const roomId = this.props.room.roomId;
|
||||
call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||
|
||||
// We don't currently show voice calls in this view when in the room:
|
||||
// they're represented in the room status bar at the bottom instead
|
||||
// (but this will all change with the new designs)
|
||||
if (call && call.type == CallType.Voice) call = null;
|
||||
} else {
|
||||
call = CallHandler.sharedInstance().getAnyActiveCall();
|
||||
// Ignore calls if we can't get the room associated with them.
|
||||
@@ -160,7 +180,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (call && call.state == CallState.Ended) return null;
|
||||
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
|
||||
return call;
|
||||
}
|
||||
|
||||
@@ -177,67 +197,240 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
let view: React.ReactNode;
|
||||
private onFullscreenClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'video_fullscreen',
|
||||
fullscreen: true,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.state.call) {
|
||||
if (this.state.call.type === "voice") {
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoom = client.getRoom(this.state.call.roomId);
|
||||
private onExpandClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.call.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
let caption = _t("Active call");
|
||||
if (this.state.isLocalOnHold) {
|
||||
// we currently have no UI for holding / unholding a call (apart from slash
|
||||
// commands) so we don't disintguish between when we've put the call on hold
|
||||
// (ie. we'd show an unhold button) and when the other side has put us on hold
|
||||
// (where obviously we would not show such a button).
|
||||
caption = _t("Call Paused");
|
||||
private onControlsHideTimer = () => {
|
||||
this.controlsHideTimer = null;
|
||||
this.setState({
|
||||
controlsVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
private onMouseMove = () => {
|
||||
this.showControls();
|
||||
}
|
||||
|
||||
private showControls() {
|
||||
if (!this.state.controlsVisible) {
|
||||
this.setState({
|
||||
controlsVisible: true,
|
||||
});
|
||||
}
|
||||
if (this.controlsHideTimer !== null) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
}
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
|
||||
private onMicMuteClick = () => {
|
||||
if (!this.state.call) return;
|
||||
|
||||
const newVal = !this.state.micMuted;
|
||||
|
||||
this.state.call.setMicrophoneMuted(newVal);
|
||||
this.setState({micMuted: newVal});
|
||||
}
|
||||
|
||||
private onVidMuteClick = () => {
|
||||
if (!this.state.call) return;
|
||||
|
||||
const newVal = !this.state.vidMuted;
|
||||
|
||||
this.state.call.setLocalVideoMuted(newVal);
|
||||
this.setState({vidMuted: newVal});
|
||||
}
|
||||
|
||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||
// Note that this assumes we always have a callview on screen at any given time
|
||||
// CallHandler would probably be a better place for this
|
||||
private onNativeKeyDown = ev => {
|
||||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.D:
|
||||
if (ctrlCmdOnly) {
|
||||
this.onMicMuteClick();
|
||||
// show the controls to give feedback
|
||||
this.showControls();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
view = <AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
|
||||
<PulsedAvatar>
|
||||
<RoomAvatar
|
||||
room={callRoom}
|
||||
height={35}
|
||||
width={35}
|
||||
/>
|
||||
</PulsedAvatar>
|
||||
<div>
|
||||
<h1>{callRoom.name}</h1>
|
||||
<p>{ caption }</p>
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
// For video calls, we currently ignore the call hold state altogether
|
||||
// (the video will just go black)
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight;
|
||||
view = <div className="mx_CallView_video" onClick={this.props.onClick}>
|
||||
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
|
||||
maxHeight={maxVideoHeight}
|
||||
/>
|
||||
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
|
||||
</div>;
|
||||
}
|
||||
case Key.E:
|
||||
if (ctrlCmdOnly) {
|
||||
this.onVidMuteClick();
|
||||
// show the controls to give feedback
|
||||
this.showControls();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let hangup: React.ReactNode;
|
||||
if (this.props.showHangup) {
|
||||
hangup = <div
|
||||
className="mx_CallView_hangup"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.state.call.roomId,
|
||||
});
|
||||
}}
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomAvatarClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.call.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.state.call) return null;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoom = client.getRoom(this.state.call.roomId);
|
||||
|
||||
let callControls;
|
||||
if (this.props.room) {
|
||||
const micClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_micOn: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_micOff: this.state.micMuted,
|
||||
});
|
||||
|
||||
const vidClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_vidOn: !this.state.vidMuted,
|
||||
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
|
||||
});
|
||||
|
||||
// Put the other states of the mic/video icons in the document to make sure they're cached
|
||||
// (otherwise the icon disappears briefly when toggled)
|
||||
const micCacheClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_micOn: this.state.micMuted,
|
||||
mx_CallView_callControls_button_micOff: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_invisible: true,
|
||||
});
|
||||
|
||||
const vidCacheClasses = classNames({
|
||||
mx_CallView_callControls_button: true,
|
||||
mx_CallView_callControls_button_vidOn: this.state.micMuted,
|
||||
mx_CallView_callControls_button_vidOff: !this.state.micMuted,
|
||||
mx_CallView_callControls_button_invisible: true,
|
||||
});
|
||||
|
||||
const callControlsClasses = classNames({
|
||||
mx_CallView_callControls: true,
|
||||
mx_CallView_callControls_hidden: !this.state.controlsVisible,
|
||||
});
|
||||
|
||||
const vidMuteButton = this.state.call.type === CallType.Video ? <div
|
||||
className={vidClasses}
|
||||
onClick={this.onVidMuteClick}
|
||||
/> : null;
|
||||
|
||||
callControls = <div className={callControlsClasses}>
|
||||
<div
|
||||
className={micClasses}
|
||||
onClick={this.onMicMuteClick}
|
||||
/>
|
||||
<div
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.state.call.roomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{vidMuteButton}
|
||||
<div className={micCacheClasses} />
|
||||
<div className={vidCacheClasses} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// The 'content' for the call, ie. the videos for a video call and profile picture
|
||||
// for voice calls (fills the bg)
|
||||
let contentView: React.ReactNode;
|
||||
|
||||
if (this.state.call.type === CallType.Video) {
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight - HEADER_HEIGHT;
|
||||
contentView = <div className="mx_CallView_video" ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
|
||||
maxHeight={maxVideoHeight}
|
||||
/>
|
||||
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
|
||||
{callControls}
|
||||
</div>;
|
||||
} else {
|
||||
const avatarSize = this.props.room ? 200 : 75;
|
||||
contentView = <div className="mx_CallView_voice" onMouseMove={this.onMouseMove}>
|
||||
<RoomAvatar
|
||||
room={callRoom}
|
||||
height={avatarSize}
|
||||
width={avatarSize}
|
||||
/>
|
||||
{callControls}
|
||||
</div>;
|
||||
}
|
||||
|
||||
const callTypeText = this.state.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
|
||||
let myClassName;
|
||||
|
||||
let fullScreenButton;
|
||||
if (this.state.call.type === CallType.Video && this.props.room) {
|
||||
fullScreenButton = <div className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
|
||||
onClick={this.onFullscreenClick} title={_t("Fill Screen")}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className={this.props.className} ref={this.container}>
|
||||
{view}
|
||||
{hangup}
|
||||
let expandButton;
|
||||
if (!this.props.room) {
|
||||
expandButton = <div className="mx_CallView_header_button mx_CallView_header_button_expand"
|
||||
onClick={this.onExpandClick} title={_t("Return to call")}
|
||||
/>;
|
||||
}
|
||||
|
||||
const headerControls = <div className="mx_CallView_header_controls">
|
||||
{fullScreenButton}
|
||||
{expandButton}
|
||||
</div>;
|
||||
|
||||
let header: React.ReactNode;
|
||||
if (this.props.room) {
|
||||
header = <div className="mx_CallView_header">
|
||||
<div className="mx_CallView_header_phoneIcon"></div>
|
||||
<span className="mx_CallView_header_callType">{callTypeText}</span>
|
||||
{headerControls}
|
||||
</div>;
|
||||
myClassName = 'mx_CallView_large';
|
||||
} else {
|
||||
header = <div className="mx_CallView_header">
|
||||
<AccessibleButton onClick={this.onRoomAvatarClick}>
|
||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||
</AccessibleButton>
|
||||
<div>
|
||||
<div className="mx_CallView_header_roomName">{callRoom.name}</div>
|
||||
<div className="mx_CallView_header_callTypeSmall">{callTypeText}</div>
|
||||
</div>
|
||||
{headerControls}
|
||||
</div>;
|
||||
myClassName = 'mx_CallView_pip';
|
||||
}
|
||||
|
||||
return <div className={"mx_CallView " + myClassName}>
|
||||
{header}
|
||||
{contentView}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||
import RoomAvatar from '../avatars/RoomAvatar';
|
||||
import FormButton from '../elements/FormButton';
|
||||
import { CallState } from 'matrix-js-sdk/lib/webrtc/call';
|
||||
@@ -108,13 +107,11 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||
|
||||
return <div className="mx_IncomingCallBox">
|
||||
<div className="mx_IncomingCallBox_CallerInfo">
|
||||
<PulsedAvatar>
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
</PulsedAvatar>
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
<div>
|
||||
<h1>{caller}</h1>
|
||||
<p>{incomingCallText}</p>
|
||||
|
||||
@@ -73,8 +73,6 @@ export default class VideoFeed extends React.Component<IProps> {
|
||||
let videoStyle = {};
|
||||
if (this.props.maxHeight) videoStyle = { maxHeight: this.props.maxHeight };
|
||||
|
||||
return <div className={classnames(videoClasses)}>
|
||||
<video ref={this.vid} style={videoStyle}></video>
|
||||
</div>;
|
||||
return <video className={classnames(videoClasses)} ref={this.vid} style={videoStyle} />;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -15,20 +15,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import * as Rooms from "./Rooms";
|
||||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import {getAddressType} from "./UserAddress";
|
||||
import { getAddressType } from "./UserAddress";
|
||||
import { getE2EEWellKnown } from "./utils/WellKnownUtils";
|
||||
import GroupStore from "./stores/GroupStore";
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import { isJoinedOrNearlyJoined } from "./utils/membership";
|
||||
|
||||
// we define a number of interfaces which take their names from the js-sdk
|
||||
/* eslint-disable camelcase */
|
||||
@@ -236,9 +237,16 @@ export function findDMForUser(client: MatrixClient, userId: string): Room {
|
||||
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const rooms = roomIds.map(id => client.getRoom(id));
|
||||
const suitableDMRooms = rooms.filter(r => {
|
||||
// Validate that we are joined and the other person is also joined. We'll also make sure
|
||||
// that the room also looks like a DM (until we have canonical DMs to tell us). For now,
|
||||
// a DM is a room of two people that contains those two people exactly. This does mean
|
||||
// that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for
|
||||
// canonical DMs to solve.
|
||||
if (r && r.getMyMembership() === "join") {
|
||||
const member = r.getMember(userId);
|
||||
return member && (member.membership === "invite" || member.membership === "join");
|
||||
const members = r.currentState.getMembers();
|
||||
const joinedMembers = members.filter(m => isJoinedOrNearlyJoined(m.membership));
|
||||
const otherMember = joinedMembers.find(m => m.userId === userId);
|
||||
return otherMember && joinedMembers.length === 2;
|
||||
}
|
||||
return false;
|
||||
}).sort((r1, r2) => {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
function onLoggedOutAndStorageCleared(): void {
|
||||
// E.g. redirect user or call other APIs after logout
|
||||
}
|
||||
|
||||
// This interface summarises all available customisation points and also marks
|
||||
// them all as optional. This allows customisers to only define and export the
|
||||
// customisations they need while still maintaining type safety.
|
||||
export interface ILifecycleCustomisations {
|
||||
onLoggedOutAndStorageCleared?: typeof onLoggedOutAndStorageCleared;
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
// customisation points that make up `ILifecycleCustomisations`.
|
||||
export default {} as ILifecycleCustomisations;
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
// Populate this file with the details of your customisations when copying it.
|
||||
|
||||
/**
|
||||
* Determines if a room is visible in the room list or not. By default,
|
||||
* all rooms are visible. Where special handling is performed by Element,
|
||||
* those rooms will not be able to override their visibility in the room
|
||||
* list - Element will make the decision without calling this function.
|
||||
*
|
||||
* This function should be as fast as possible to avoid slowing down the
|
||||
* client.
|
||||
* @param {Room} room The room to check the visibility of.
|
||||
* @returns {boolean} True if the room should be visible, false otherwise.
|
||||
*/
|
||||
function isRoomVisible(room: Room): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// This interface summarises all available customisation points and also marks
|
||||
// them all as optional. This allows customisers to only define and export the
|
||||
// customisations they need while still maintaining type safety.
|
||||
export interface IRoomListCustomisations {
|
||||
isRoomVisible?: typeof isRoomVisible;
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
// customisation points that make up the interface above.
|
||||
export const RoomListCustomisations: IRoomListCustomisations = {};
|
||||
@@ -67,24 +67,13 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
|
||||
// them all as optional. This allows customisers to only define and export the
|
||||
// customisations they need while still maintaining type safety.
|
||||
export interface ISecurityCustomisations {
|
||||
examineLoginResponse?: (
|
||||
response: any,
|
||||
credentials: IMatrixClientCreds,
|
||||
) => void;
|
||||
persistCredentials?: (
|
||||
credentials: IMatrixClientCreds,
|
||||
) => void;
|
||||
createSecretStorageKey?: () => Uint8Array,
|
||||
getSecretStorageKey?: () => Uint8Array,
|
||||
catchAccessSecretStorageError?: (
|
||||
e: Error,
|
||||
) => void,
|
||||
setupEncryptionNeeded?: (
|
||||
kind: SetupEncryptionKind,
|
||||
) => boolean,
|
||||
getDehydrationKey?: (
|
||||
keyInfo: ISecretStorageKeyInfo,
|
||||
) => Promise<Uint8Array>,
|
||||
examineLoginResponse?: typeof examineLoginResponse;
|
||||
persistCredentials?: typeof persistCredentials;
|
||||
createSecretStorageKey?: typeof createSecretStorageKey,
|
||||
getSecretStorageKey?: typeof getSecretStorageKey,
|
||||
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError,
|
||||
setupEncryptionNeeded?: typeof setupEncryptionNeeded,
|
||||
getDehydrationKey?: typeof getDehydrationKey,
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Populate this class with the details of your customisations when copying it.
|
||||
import { Capability, Widget } from "matrix-widget-api";
|
||||
|
||||
/**
|
||||
* Approves the widget for capabilities that it requested, if any can be
|
||||
* approved. Typically this will be used to give certain widgets capabilities
|
||||
* without having to prompt the user to approve them. This cannot reject
|
||||
* capabilities that Element will be automatically granting, such as the
|
||||
* ability for Jitsi widgets to stay on screen - those will be approved
|
||||
* regardless.
|
||||
* @param {Widget} widget The widget to approve capabilities for.
|
||||
* @param {Set<Capability>} requestedCapabilities The capabilities the widget requested.
|
||||
* @returns {Set<Capability>} Resolves to the capabilities that are approved for use
|
||||
* by the widget. If none are approved, this should return an empty Set.
|
||||
*/
|
||||
async function preapproveCapabilities(
|
||||
widget: Widget,
|
||||
requestedCapabilities: Set<Capability>,
|
||||
): Promise<Set<Capability>> {
|
||||
return new Set(); // no additional capabilities approved
|
||||
}
|
||||
|
||||
// This interface summarises all available customisation points and also marks
|
||||
// them all as optional. This allows customisers to only define and export the
|
||||
// customisations they need while still maintaining type safety.
|
||||
export interface IWidgetPermissionCustomisations {
|
||||
preapproveCapabilities?: typeof preapproveCapabilities;
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
// customisation points that make up the interface above.
|
||||
export const WidgetPermissionCustomisations: IWidgetPermissionCustomisations = {};
|
||||
@@ -21,6 +21,7 @@ import { walkDOMDepthFirst } from "./dom";
|
||||
import { checkBlockNode } from "../HtmlUtils";
|
||||
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
|
||||
import { PartCreator } from "./parts";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
|
||||
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
||||
const ATROOM = "@room";
|
||||
@@ -130,6 +131,23 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "DIV":
|
||||
case "SPAN": {
|
||||
// math nodes are translated back into delimited latex strings
|
||||
if (n.hasAttribute("data-mx-maths")) {
|
||||
const delimLeft = (n.nodeName == "SPAN") ?
|
||||
(SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$" :
|
||||
(SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$";
|
||||
const delimRight = (n.nodeName == "SPAN") ?
|
||||
(SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$" :
|
||||
(SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$";
|
||||
const tex = n.getAttribute("data-mx-maths");
|
||||
return partCreator.plain(delimLeft + tex + delimRight);
|
||||
} else if (!checkDescendInto(n)) {
|
||||
return partCreator.plain(n.textContent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "OL":
|
||||
state.listIndex.push((<HTMLOListElement>n).start || 1);
|
||||
/* falls through */
|
||||
|
||||
+39
-2
@@ -18,6 +18,10 @@ limitations under the License.
|
||||
import Markdown from '../Markdown';
|
||||
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
|
||||
import EditorModel from "./model";
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import SettingsStore from '../settings/SettingsStore';
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import cheerio from 'cheerio';
|
||||
|
||||
export function mdSerialize(model: EditorModel) {
|
||||
return model.parts.reduce((html, part) => {
|
||||
@@ -38,10 +42,43 @@ export function mdSerialize(model: EditorModel) {
|
||||
}
|
||||
|
||||
export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
|
||||
const md = mdSerialize(model);
|
||||
let md = mdSerialize(model);
|
||||
|
||||
if (SettingsStore.getValue("feature_latex_maths")) {
|
||||
const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
|
||||
"\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
|
||||
const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
|
||||
"\\$(([^$]|\\\\\\$)*)\\$";
|
||||
|
||||
md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
|
||||
const p1e = AllHtmlEntities.encode(p1);
|
||||
return `<div data-mx-maths="${p1e}">\n\n</div>\n\n`;
|
||||
});
|
||||
|
||||
md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
|
||||
const p1e = AllHtmlEntities.encode(p1);
|
||||
return `<span data-mx-maths="${p1e}"></span>`;
|
||||
});
|
||||
|
||||
// make sure div tags always start on a new line, otherwise it will confuse
|
||||
// the markdown parser
|
||||
md = md.replace(/(.)<div/g, function(m, p1) { return `${p1}\n<div`; });
|
||||
}
|
||||
|
||||
const parser = new Markdown(md);
|
||||
if (!parser.isPlainText() || forceHTML) {
|
||||
return parser.toHTML();
|
||||
// feed Markdown output to HTML parser
|
||||
const phtml = cheerio.load(parser.toHTML(),
|
||||
{ _useHtmlParser2: true, decodeEntities: false })
|
||||
|
||||
// add fallback output for latex math, which should not be interpreted as markdown
|
||||
phtml('div, span').each(function(i, e) {
|
||||
const tex = phtml(e).attr('data-mx-maths')
|
||||
if (tex) {
|
||||
phtml(e).html(`<code>${tex}</code>`)
|
||||
}
|
||||
});
|
||||
return phtml.html();
|
||||
}
|
||||
// ensure removal of escape backslashes in non-Markdown messages
|
||||
if (md.indexOf("\\") > -1) {
|
||||
|
||||
+1128
-5
File diff suppressed because it is too large
Load Diff
+184
-1
@@ -2347,5 +2347,188 @@
|
||||
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Всички сървъри за възбранени от участие! Тази стая вече не може да бъде използвана.",
|
||||
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s промени сървърните разрешения за контрол на достъпа до тази стая.",
|
||||
"Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Добавя ( ͡° ͜ʖ ͡°) в началото на текстовото съобщение",
|
||||
"The call was answered on another device.": "На обаждането беше отговорено от друго устройство."
|
||||
"The call was answered on another device.": "На обаждането беше отговорено от друго устройство.",
|
||||
"This room is public": "Тази стая е публична",
|
||||
"Move right": "Премести надясно",
|
||||
"Move left": "Премести наляво",
|
||||
"Revoke permissions": "Оттеглете привилегии",
|
||||
"Take a picture": "Направете снимка",
|
||||
"Unable to set up keys": "Неуспешна настройка на ключовете",
|
||||
"Use your Security Key to continue.": "Използвайте ключа си за сигурност за да продължите.",
|
||||
"Security Key": "Ключ за сигурност",
|
||||
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Въведете защитната фраза или <button>използвайте ключа за сигурност</button> за да продължите.",
|
||||
"Security Phrase": "Защитна фраза",
|
||||
"Invalid Recovery Key": "Невалиден ключ за възстановяване",
|
||||
"Wrong Recovery Key": "Грешен ключ за възстановяване",
|
||||
"Looks good!": "Изглежда добре!",
|
||||
"Wrong file type": "Грешен тип файл",
|
||||
"Recent changes that have not yet been received": "Скорошни промени, които още не са били получени",
|
||||
"The server is not configured to indicate what the problem is (CORS).": "Сървърът не е конфигуриран да укаже какъв е проблемът (CORS).",
|
||||
"A connection error occurred while trying to contact the server.": "Възникнал е проблем с връзката при свързване към сървъра.",
|
||||
"Your area is experiencing difficulties connecting to the internet.": "В районът ви има проблеми с връзката с интернет.",
|
||||
"The server has denied your request.": "Сървърът е забранил заявката ви.",
|
||||
"The server is offline.": "Сървърът е офлайн.",
|
||||
"A browser extension is preventing the request.": "Разширение на браузъра блокира заявката.",
|
||||
"Your firewall or anti-virus is blocking the request.": "Защитната ви стена (firewall) или антивирусен софтуер блокират заявката.",
|
||||
"The server (%(serverName)s) took too long to respond.": "Сървърът %(serverName)s отне твърде дълго да отговори.",
|
||||
"Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Сървърът ви не отговаря на някой от заявките ви. По-долу са някои от най-вероятните причини.",
|
||||
"Server isn't responding": "Сървърът не отговаря",
|
||||
"You're all caught up.": "Наваксали сте с всичко.",
|
||||
"Data on this screen is shared with %(widgetDomain)s": "Данните на този екран са споделени с %(widgetDomain)s",
|
||||
"Modal Widget": "Модално приспособление",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Поканете някой по име, потребителско име (като <userId/>) или <a>споделете тази стая</a>.",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Поканете някой по име, имейл адрес, потребителско име (като <userId/>) или <a>споделете тази стая</a>.",
|
||||
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Това няма да ги покани в %(communityName)s. За да поканите някой в %(communityName)s, кликнете <a>тук</a>",
|
||||
"Start a conversation with someone using their name or username (like <userId/>).": "Започнете разговор с някой използвайки тяхното име или потребителско име (като <userId/>).",
|
||||
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Започнете разговор с някой използвайки тяхното име, имейл адрес или потребителско име (като <userId/>).",
|
||||
"May include members not in %(communityName)s": "Може да съдържа членове, които не са в %(communityName)s",
|
||||
"Invite by email": "Покани по имейл",
|
||||
"Send feedback": "Изпрати обратна връзка",
|
||||
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "ПРОФЕСИОНАЛЕН СЪВЕТ: Ако ще съобщавате за проблем, изпратете и <debugLogsLink>логове за разработчици</debugLogsLink> за да ни помогнете да открием проблема.",
|
||||
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Първо прегледайте <existingIssuesLink>съществуващите проблеми в Github</existingIssuesLink>. Няма подобни? <newIssueLink>Създайте нов</newIssueLink>.",
|
||||
"Report a bug": "Съобщете за проблем",
|
||||
"There are two ways you can provide feedback and help us improve %(brand)s.": "Има два начина да дадете обратна връзка и да ни помогнете да подобрим %(brand)s.",
|
||||
"Comment": "Коментар",
|
||||
"Add comment": "Добави коментар",
|
||||
"Please go into as much detail as you like, so we can track down the problem.": "Моля, разкажете колкото подробно желаете, за да можем да открием проблема.",
|
||||
"Tell us below how you feel about %(brand)s so far.": "Кажете ни какво мислите за %(brand)s към този момент.",
|
||||
"Rate %(brand)s": "Оценете %(brand)s",
|
||||
"Feedback sent": "Обратната връзка беше изпратена",
|
||||
"Update community": "Обнови общността",
|
||||
"There was an error updating your community. The server is unable to process your request.": "Възникна грешка при обновяването на общността. Сървърът не може да обработки заявката ви.",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Блокирай всеки, който не е част от %(serverName)s от присъединяване в тази стая.",
|
||||
"Create a room in %(communityName)s": "Създай стая в %(communityName)s",
|
||||
"You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Може да изключите това, ако стаята ще се използва за съвместна работа с външни екипи, имащи собствен сървър. Това не може да бъде променено по-късно.",
|
||||
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Може да включите това, ако стаята ще се използва само за съвместна работа на вътрешни екипи на сървъра ви. Това не може да бъде променено по-късно.",
|
||||
"Your server requires encryption to be enabled in private rooms.": "Сървърът ви изисква в частните стаи да е включено шифроване.",
|
||||
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Към частните стаи присъединяването е само с покана. Публичните могат да бъдат открити и присъединени от всеки в тази общност.",
|
||||
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Към частните стаи присъединяването е само с покана. Публичните могат да бъдат открити и присъединени от всеки.",
|
||||
"An image will help people identify your community.": "Снимката би помогнала на хората да идентифицират общността ви.",
|
||||
"Add image (optional)": "Добавете снимка (незадължително)",
|
||||
"Enter name": "Въведете име",
|
||||
"What's the name of your community or team?": "Какво е името на общността или екипа ви?",
|
||||
"You can change this later if needed.": "Може да промените това и по-късно, ако е нужно.",
|
||||
"Use this when referencing your community to others. The community ID cannot be changed.": "Използвайте това когато се обръщате към общността сред други хора. Идентификатора не може да бъде променен.",
|
||||
"Community ID: +<localpart />:%(domain)s": "Идентификатор на общност: +<localpart />:%(domain)s",
|
||||
"There was an error creating your community. The name may be taken or the server is unable to process your request.": "Възникна грешка при създаването на общността. Името може да е заето или пък сървърът не може да обработи заявката.",
|
||||
"Invite people to join %(communityName)s": "Поканете хора в %(communityName)s",
|
||||
"Send %(count)s invites|one": "Изпрати %(count)s покана",
|
||||
"Send %(count)s invites|other": "Изпрати %(count)s покани",
|
||||
"People you know on %(brand)s": "Хора, които познаване в %(brand)s",
|
||||
"Add another email": "Добави друг имейл",
|
||||
"Download logs": "Изтегли на логове",
|
||||
"Preparing to download logs": "Подготвяне за изтегляне на логове",
|
||||
"Information": "Информация",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Тази версия на %(brand)s не поддържа търсенето в шифровани съобщения",
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Тази версия на %(brand)s не поддържа преглеждането на някои шифровани файлове",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "Използвайте <a>Desktop приложението</a> за да търсите в шифровани съобщения",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Използвайте <a>Desktop приложението</a> за да видите всички шифровани файлове",
|
||||
"Click to view edits": "Кликнете за да видите редакциите",
|
||||
"Edited at %(date)s": "Редактирано на %(date)s",
|
||||
"Message deleted on %(date)s": "Съобщението изтрито на %(date)s",
|
||||
"Video conference started by %(senderName)s": "Видео конференцията беше стартирана от %(senderName)s",
|
||||
"Video conference updated by %(senderName)s": "Видео конференцията беше обновена от %(senderName)s",
|
||||
"Video conference ended by %(senderName)s": "Видео конференцията беше прекратена от %(senderName)s",
|
||||
"Join the conference from the room information card on the right": "Присъединете се към конференцията от информацията за стаята в дясно",
|
||||
"Join the conference at the top of this room": "Присъединете се към конференцията в горната част на стаята",
|
||||
"Ignored attempt to disable encryption": "Опитът за изключване на шифроването беше игнориран",
|
||||
"Room settings": "Настройки на стаята",
|
||||
"Show files": "Покажи файловете",
|
||||
"%(count)s people|other": "%(count)s човека",
|
||||
"%(count)s people|one": "%(count)s човек",
|
||||
"About": "Относно",
|
||||
"Not encrypted": "Не е шифровано",
|
||||
"Add widgets, bridges & bots": "Добави приспособления, мостове и ботове",
|
||||
"Edit widgets, bridges & bots": "Промени приспособления, мостове и ботове",
|
||||
"Widgets": "Приспособления",
|
||||
"Unpin a widget to view it in this panel": "Разкачете приспособление за да го видите в този панел",
|
||||
"Unpin": "Разкачи",
|
||||
"You can only pin up to %(count)s widgets|other": "Може да закачите максимум %(count)s приспособления",
|
||||
"Room Info": "Информация за стаята",
|
||||
"Favourited": "В любими",
|
||||
"Forget Room": "Забрави стаята",
|
||||
"Notification options": "Настройки за уведомление",
|
||||
"Mentions & Keywords": "Споменавания и ключови думи",
|
||||
"Use default": "Използвай по подразбиране",
|
||||
"Show previews of messages": "Показвай преглед на съобщенията",
|
||||
"Show rooms with unread messages first": "Показвай стаи с непрочетени съобщения първи",
|
||||
"%(count)s results|one": "%(count)s резултат",
|
||||
"%(count)s results|other": "%(count)s резултата",
|
||||
"Use the + to make a new room or explore existing ones below": "Използвайте + за да направите нова стая или да прегледате съществуващите",
|
||||
"Explore all public rooms": "Прегледай всички публични стаи",
|
||||
"Can't see what you’re looking for?": "Не намирате това, което търсите?",
|
||||
"Custom Tag": "Собствен етикет",
|
||||
"Explore public rooms": "Прегледай публични стаи",
|
||||
"Explore community rooms": "Прегледай стаи от общността",
|
||||
"Show Widgets": "Покажи приспособленията",
|
||||
"Hide Widgets": "Скрий приспособленията",
|
||||
"Remove messages sent by others": "Премахвай съобщения изпратени от други",
|
||||
"Privacy": "Поверителност",
|
||||
"Secure Backup": "Защитено резервно копие",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Настройте името на шрифт инсталиран в системата и %(brand)s ще се опита да го използва.",
|
||||
"not ready": "не е готово",
|
||||
"ready": "готово",
|
||||
"Secret storage:": "Секретно складиране:",
|
||||
"Backup key cached:": "Резервният ключ е кеширан:",
|
||||
"Backup key stored:": "Резервният ключ е съхранен:",
|
||||
"Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Правете резервно копие на ключовете за шифроване и данните в профила, в случай че загубите достъп до сесиите си. Ключовете ви ще бъдат защитени с уникален ключ за възстановяване.",
|
||||
"Algorithm:": "Алгоритъм:",
|
||||
"Backup version:": "Версия на резервното копие:",
|
||||
"The operation could not be completed": "Операцията не можа да бъде завършена",
|
||||
"Failed to save your profile": "Неуспешно запазване на профила ви",
|
||||
"You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Вероятно сте ги конфигурирали в клиент различен от %(brand)s. Не можете да ги управлявате в %(brand)s, но те все пак важат.",
|
||||
"There are advanced notifications which are not shown here.": "Съществуват по-сложни уведомления, които не са показани тук.",
|
||||
"%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s не може да кешира шифровани съобщения локално по сигурен начин когато работи в уеб браузър. Използвайте <desktopLink>%(brand)s Desktop </desktopLink> за да можете да търсите шифровани съобщения.",
|
||||
"Master private key:": "Главен частен ключ:",
|
||||
"not found in storage": "не е намерено в складирането",
|
||||
"Cross-signing is not set up.": "Кръстосаното-подписване не е настроено.",
|
||||
"Cross-signing is ready for use.": "Кръстосаното-подписване е готово за използване.",
|
||||
"Your server isn't responding to some <a>requests</a>.": "Сървърът ви не отговаря на някои <a>заявки</a>.",
|
||||
"%(senderName)s ended the call": "%(senderName)s приключи разговора",
|
||||
"You ended the call": "Приключихте разговора",
|
||||
"New version of %(brand)s is available": "Налична е нова версия на %(brand)s",
|
||||
"Update %(brand)s": "Обнови %(brand)s",
|
||||
"Enable desktop notifications": "Включете уведомления на работния плот",
|
||||
"Don't miss a reply": "Не пропускайте отговор",
|
||||
"User menu": "Потребителско меню",
|
||||
"Search rooms": "Търси стаи",
|
||||
"Save your Security Key": "Запази ключа за сигурност",
|
||||
"Confirm Security Phrase": "Потвърди фразата за сигурност",
|
||||
"Set a Security Phrase": "Настрой фраза за сигурност",
|
||||
"You can also set up Secure Backup & manage your keys in Settings.": "Също така, може да конфигурирате защитено резервно копиране и да управлявате ключовете си от Настройки.",
|
||||
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Ако се откажете сега, може да загубите достъп до шифрованите съобщения и данни, в случай че загубите достъп до тази сесия.",
|
||||
"Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Запазете ключа за сигурност на сигурно място, като password manager или сейф, понеже се използва за предпазване на шифрованите данни.",
|
||||
"Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Въведете фраза за сигурност, която знаете само вие. Тя ще се използва за предпазване на данните ви. За да е по-сигурно, не използвайте паролата за профила си.",
|
||||
"Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Използвайте секретна фраза, която знаете само вие. При необходимост запазете и ключа за сигурност за резервното копие.",
|
||||
"Enter a Security Phrase": "Въведете фраза за сигурност",
|
||||
"We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Ще генерираме ключ за сигурност, който да съхраните на сигурно място, като password manager или сейф.",
|
||||
"Generate a Security Key": "Генерирай ключ за сигурност",
|
||||
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Предпазете се от загуба на достъп до шифрованите съобщения и данни като направите резервно копие на ключовете за шифроване върху сървъра.",
|
||||
"Now, let's help you get started": "Нека ви помогнем да започнете",
|
||||
"Welcome %(name)s": "Добре дошли, %(name)s",
|
||||
"Add a photo so people know it's you.": "Добавете снимка, за да може другите хора да знаят, че сте вие.",
|
||||
"Great, that'll help people know it's you": "Чудесно, това ще позволи на хората да знаят, че сте вие",
|
||||
"Starting microphone...": "Стартиране на микрофона...",
|
||||
"Starting camera...": "Стартиране на камерата...",
|
||||
"Call connecting...": "Свързване на разговор...",
|
||||
"Calling...": "Звънене...",
|
||||
"You do not have permission to create rooms in this community.": "Нямате привилегии да създавате стаи в тази общност.",
|
||||
"Cannot create rooms in this community": "Не можете да създавате стаи в тази общност",
|
||||
"Community and user menu": "Меню за общността и потребителя",
|
||||
"User settings": "Потребителски настройки",
|
||||
"Community settings": "Настройки на общност",
|
||||
"Failed to find the general chat for this community": "Неуспешно откриване на основния чат за тази общност",
|
||||
"Create community": "Създай общност",
|
||||
"Explore rooms in %(communityName)s": "Преглед на стаи в %(communityName)s",
|
||||
"%(brand)s Android": "%(brand)s за Android",
|
||||
"You have no visible notifications in this room.": "Нямате видими уведомления за тази стая.",
|
||||
"You’re all caught up": "Наваксали сте с всичко",
|
||||
"Attach files from chat or just drag and drop them anywhere in a room.": "Прикачете файлове от чата или ги издърпайте и пуснете в стаята.",
|
||||
"No files visible in this room": "Няма видими файлове в тази стая",
|
||||
"Away": "Отсъства",
|
||||
"%(brand)s iOS": "%(brand)s за iOS",
|
||||
"%(brand)s Desktop": "%(brand)s Desktop",
|
||||
"%(brand)s Web": "Уеб версия на %(brand)s",
|
||||
"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>.": "Въведете адреса на вашия Element Matrix Services сървър. Той или използва ваш собствен домейн или е поддомейн на <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.": "Може да използвате опцията за собствен сървър, за да влезете в друг Matrix сървър, чрез указване на адреса му. Това позволява да използвате %(brand)s с Matrix профил съществуващ на друг сървър."
|
||||
}
|
||||
|
||||
+271
-134
@@ -15,9 +15,9 @@
|
||||
"Favourite": "Favorit",
|
||||
"Mute": "Silencia",
|
||||
"Room directory": "Directori de sales",
|
||||
"Settings": "Paràmetres",
|
||||
"Settings": "Configuració",
|
||||
"Start chat": "Inicia un xat",
|
||||
"Failed to change password. Is your password correct?": "Hi ha hagut un error al canviar la vostra contrasenya. És correcte la vostra contrasenya?",
|
||||
"Failed to change password. Is your password correct?": "S'ha produït un error en canviar la contrasenya. És correcta la teva contrasenya?",
|
||||
"Continue": "Continua",
|
||||
"Custom Server Options": "Opcions de servidor personalitzat",
|
||||
"Dismiss": "Omet",
|
||||
@@ -29,7 +29,7 @@
|
||||
"Search": "Cerca",
|
||||
"powered by Matrix": "amb tecnologia de Matrix",
|
||||
"Edit": "Edita",
|
||||
"Unpin Message": "Desenganxa el missatge",
|
||||
"Unpin Message": "Anul·la la fixació de missatge",
|
||||
"Register": "Registre",
|
||||
"Rooms": "Sales",
|
||||
"Add rooms to this community": "Afegeix sales a aquesta comunitat",
|
||||
@@ -37,17 +37,17 @@
|
||||
"Guests can join": "Els usuaris d'altres xarxes s'hi poden unir",
|
||||
"This email address is already in use": "Aquesta adreça de correu electrònic ja està en ús",
|
||||
"This phone number is already in use": "Aquest número de telèfon ja està en ús",
|
||||
"Failed to verify email address: make sure you clicked the link in the email": "No s'ha pogut verificar l'adreça de correu electrònic. Assegureu-vos de fer clic a l'enllaç del correu electrònic de verificació",
|
||||
"Failed to verify email address: make sure you clicked the link in the email": "No s'ha pogut verificar l'adreça de correu electrònic: assegura't de fer clic a l'enllaç del correu electrònic",
|
||||
"Call Failed": "No s'ha pogut realitzar la trucada",
|
||||
"The remote side failed to pick up": "El costat remot no ha contestat",
|
||||
"The remote side failed to pick up": "El part remota no ha contestat",
|
||||
"Unable to capture screen": "No s'ha pogut capturar la pantalla",
|
||||
"Existing Call": "Trucada existent",
|
||||
"You are already in a call.": "Ja sou a una trucada.",
|
||||
"VoIP is unsupported": "El VoIP no és compatible",
|
||||
"You cannot place VoIP calls in this browser.": "No es poden fer trucades VoIP amb aquest navegador.",
|
||||
"You cannot place a call with yourself.": "No és possible trucar-se a un mateix.",
|
||||
"You are already in a call.": "Ja ets en una trucada.",
|
||||
"VoIP is unsupported": "VoIP no és compatible",
|
||||
"You cannot place VoIP calls in this browser.": "No pots fer trucades VoIP en aquest navegador.",
|
||||
"You cannot place a call with yourself.": "No pots trucar-te a tu mateix.",
|
||||
"Warning!": "Avís!",
|
||||
"Upload Failed": "No s'ha pogut realitzar la pujada",
|
||||
"Upload Failed": "No s'ha pogut pujar",
|
||||
"Sun": "dg.",
|
||||
"Mon": "dl.",
|
||||
"Tue": "dt.",
|
||||
@@ -69,51 +69,51 @@
|
||||
"Dec": "des.",
|
||||
"PM": "PM",
|
||||
"AM": "AM",
|
||||
"%(weekDayName)s %(time)s": "%(weekDayName)s%(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s de %(monthName)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s de %(monthName)s de %(fullYear)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s de %(monthName)s de %(fullYear)s %(time)s",
|
||||
"Who would you like to add to this community?": "A qui voleu afegir a aquesta comunitat?",
|
||||
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Avís: les persones que afegiu a aquesta comunitat seran visibles públicament per a qualsevol que conegui l'ID de la comunitat",
|
||||
"Invite new community members": "Convida nous membres a unir-se a la comunitat",
|
||||
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s de/d' %(monthName)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s de/d' %(monthName)s de %(fullYear)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s de/d' %(monthName)s de %(fullYear)s %(time)s",
|
||||
"Who would you like to add to this community?": "A qui vols afegir a aquesta comunitat?",
|
||||
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Avís: qualsevol persona que afegeixis a una comunitat serà visible públicament per a qualsevol que conegui l'ID de la comunitat",
|
||||
"Invite new community members": "Convida nous membres a la comunitat",
|
||||
"Invite to Community": "Convida a la comunitat",
|
||||
"Which rooms would you like to add to this community?": "Quines sales voleu afegir a aquesta comunitat?",
|
||||
"Show these rooms to non-members on the community page and room list?": "Voleu mostrar aquestes sales als que no son membres a la pàgina de la comunitat i a la llista de sales?",
|
||||
"Which rooms would you like to add to this community?": "Quines sales vols afegir a aquesta comunitat?",
|
||||
"Show these rooms to non-members on the community page and room list?": "Vols mostrar aquestes sales a la pàgina de la comunitat i a la llista de sales per als que no hi son membres?",
|
||||
"Add rooms to the community": "Afegeix sales a la comunitat",
|
||||
"Add to community": "Afegeix a la comunitat",
|
||||
"Failed to invite the following users to %(groupId)s:": "No s'ha pogut convidar a %(groupId)s els següents usuaris:",
|
||||
"Failed to invite users to community": "No s'ha pogut convidar als usuaris a la comunitat",
|
||||
"Failed to invite users to %(groupId)s": "No s'ha pogut convidar els usuaris a %(groupId)s",
|
||||
"Failed to add the following rooms to %(groupId)s:": "No s'ha pogut afegir les següents sales al %(groupId)s:",
|
||||
"%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s no té permís per enviar-vos notificacions. Comproveu la configuració del vostre navegador",
|
||||
"%(brand)s was not given permission to send notifications - please try again": "%(brand)s no ha rebut cap permís per enviar notificacions. Torneu-ho a provar",
|
||||
"Unable to enable Notifications": "No s'ha pogut activar les notificacions",
|
||||
"Failed to invite the following users to %(groupId)s:": "No s'han pogut convidar a %(groupId)s els següents usuaris:",
|
||||
"Failed to invite users to community": "No s'han pogut convidar els usuaris a la comunitat",
|
||||
"Failed to invite users to %(groupId)s": "No s'han pogut convidar els usuaris a %(groupId)s",
|
||||
"Failed to add the following rooms to %(groupId)s:": "No s'han pogut afegir a %(groupId)s les següents sales:",
|
||||
"%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s no té permís per enviar-te notificacions, comprova la configuració del teu navegador",
|
||||
"%(brand)s was not given permission to send notifications - please try again": "%(brand)s no ha rebut cap permís per enviar notificacions, torna-ho a provar",
|
||||
"Unable to enable Notifications": "No s'han pogut activar les notificacions",
|
||||
"This email address was not found": "Aquesta adreça de correu electrònic no s'ha trobat",
|
||||
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "La vostra adreça de correu electrònic no sembla que estigui associada amb un identificador de Matrix d'aquest servidor.",
|
||||
"Default": "Per defecte",
|
||||
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "La teva adreça de correu electrònic no sembla estar associada amb un ID de Matrix en aquest servidor.",
|
||||
"Default": "Predeterminat",
|
||||
"Restricted": "Restringit",
|
||||
"Moderator": "Moderador",
|
||||
"Admin": "Administrador",
|
||||
"Failed to invite": "No s'ha pogut tramitar la invitació",
|
||||
"Failed to invite": "No s'ha pogut convidar",
|
||||
"Failed to invite the following users to the %(roomName)s room:": "No s'ha pogut convidar a la sala %(roomName)s els següents usuaris:",
|
||||
"You need to be logged in.": "És necessari estar autenticat.",
|
||||
"You need to be able to invite users to do that.": "Per poder fer això, heu de poder convidar a altres usuaris.",
|
||||
"You need to be logged in.": "Has d'haver iniciat sessió.",
|
||||
"You need to be able to invite users to do that.": "Per fer això, necessites poder convidar a usuaris.",
|
||||
"Unable to create widget.": "No s'ha pogut crear el giny.",
|
||||
"Failed to send request.": "No s'ha pogut enviar la sol·licitud.",
|
||||
"This room is not recognised.": "No es reconeix aquesta sala.",
|
||||
"Power level must be positive integer.": "El nivell de poders ha de ser un enter positiu.",
|
||||
"You are not in this room.": "No heu entrat a aquesta sala.",
|
||||
"You do not have permission to do that in this room.": "No teniu el permís per realitzar aquesta acció en aquesta sala.",
|
||||
"Missing room_id in request": "Falta el room_id en la vostra sol·licitud",
|
||||
"Room %(roomId)s not visible": "La sala %(roomId)s no és visible",
|
||||
"Missing user_id in request": "Falta l'user_id a la vostra sol·licitud",
|
||||
"Power level must be positive integer.": "El nivell d'autoritat ha de ser un enter positiu.",
|
||||
"You are not in this room.": "No ets en aquesta sala.",
|
||||
"You do not have permission to do that in this room.": "No tens permís per fer això en aquesta sala.",
|
||||
"Missing room_id in request": "Falta el room_id a la sol·licitud",
|
||||
"Room %(roomId)s not visible": "Sala %(roomId)s no visible",
|
||||
"Missing user_id in request": "Falta l'user_id a la sol·licitud",
|
||||
"Usage": "Ús",
|
||||
"/ddg is not a command": "/ddg no és un comandament",
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "Per utilitzar-lo, simplement espereu que es completin els resultats automàticament i seleccioneu-ne el desitjat.",
|
||||
"/ddg is not a command": "/ddg no és una ordre",
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "Per utilitzar-ho, simplement espera que es completin els resultats automàticament i clica'n el desitjat.",
|
||||
"Ignored user": "Usuari ignorat",
|
||||
"You are now ignoring %(userId)s": "Esteu ignorant l'usuari %(userId)s",
|
||||
"You are now ignoring %(userId)s": "Estàs ignorant l'usuari %(userId)s",
|
||||
"Unignored user": "Usuari no ignorat",
|
||||
"You are no longer ignoring %(userId)s": "Ja no esteu ignorant l'usuari %(userId)s",
|
||||
"You are no longer ignoring %(userId)s": "Ja no estàs ignorant l'usuari %(userId)s",
|
||||
"Verified key": "Claus verificades",
|
||||
"Call Timeout": "Temps d'espera de les trucades",
|
||||
"Reason": "Raó",
|
||||
@@ -122,7 +122,7 @@
|
||||
"%(senderName)s requested a VoIP conference.": "%(senderName)s ha sol·licitat una conferència VoIP.",
|
||||
"%(senderName)s invited %(targetName)s.": "%(senderName)s ha convidat a %(targetName)s.",
|
||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s ha expulsat a %(targetName)s.",
|
||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s ha establert %(displayName)s com el seu nom visible.",
|
||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s han establert el seu nom visible a %(displayName)s.",
|
||||
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s ha retirat el seu nom visible %(oldDisplayName)s.",
|
||||
"%(senderName)s removed their profile picture.": "%(senderName)s ha retirat la seva foto de perfil.",
|
||||
"%(senderName)s changed their profile picture.": "%(senderName)s ha canviat la seva foto de perfil.",
|
||||
@@ -147,19 +147,19 @@
|
||||
"(unknown failure: %(reason)s)": "(error desconegut: %(reason)s)",
|
||||
"%(senderName)s ended the call.": "%(senderName)s ha penjat.",
|
||||
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s ha convidat a %(targetDisplayName)s a entrar a la sala.",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s ha fet visible l'històric futur de la sala per a tots els membres, a partir de que hi són convidats.",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s ha fet visible l'històric futur de la sala a tots els membres, des de que entren a la sala.",
|
||||
"%(senderName)s made future room history visible to all room members.": "%(senderName)s ha fet visible l'històric futur de la sala a tots els membres de la sala.",
|
||||
"%(senderName)s made future room history visible to anyone.": "%(senderName)s ha fet visible el futur historial de la sala per a tothom.",
|
||||
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s ha fet visible el futur historial de la sala per a desconeguts (%(visibility)s).",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a tots els seus membres, a partir de que hi són convidats.",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a tots els seus membres des de que s'hi uneixen.",
|
||||
"%(senderName)s made future room history visible to all room members.": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a tots els seus membres.",
|
||||
"%(senderName)s made future room history visible to anyone.": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a tothom.",
|
||||
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s ha establert la visibilitat de l'historial futur de la sala a desconegut (%(visibility)s).",
|
||||
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s de %(fromPowerLevel)s a %(toPowerLevel)s",
|
||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ha canviat el nivell de poders de %(powerLevelDiffText)s.",
|
||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ha canviat el nivell d'autoritat de %(powerLevelDiffText)s.",
|
||||
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s ha canviat els missatges fixats de la sala.",
|
||||
"%(widgetName)s widget modified by %(senderName)s": "%(senderName)s ha modificat el giny %(widgetName)s",
|
||||
"%(widgetName)s widget added by %(senderName)s": "%(senderName)s ha afegit el giny %(widgetName)s",
|
||||
"%(widgetName)s widget removed by %(senderName)s": "%(senderName)s ha eliminat el giny %(widgetName)s",
|
||||
"Failure to create room": "No s'ha pogut crear la sala",
|
||||
"Server may be unavailable, overloaded, or you hit a bug.": "És possible que el servidor no estigui disponible, amb sobrecàrrega o que s'hagi trobat un error.",
|
||||
"Server may be unavailable, overloaded, or you hit a bug.": "És possible que el servidor no estigui disponible, sobrecarregat o que hagi topat amb un error.",
|
||||
"Send": "Envia",
|
||||
"Unnamed Room": "Sala sense nom",
|
||||
"Your browser does not support the required cryptography extensions": "El vostre navegador no és compatible amb els complements criptogràfics necessaris",
|
||||
@@ -167,7 +167,7 @@
|
||||
"Authentication check failed: incorrect password?": "Ha fallat l'autenticació: heu introduït correctament la contrasenya?",
|
||||
"Failed to join room": "No s'ha pogut entrar a la sala",
|
||||
"Message Pinning": "Fixació de missatges",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostra les marques de temps en format de 12 hores (per exemple, 2:30pm)",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostra les marques de temps en format de 12 hores (p.e. 2:30pm)",
|
||||
"Autoplay GIFs and videos": "Reprodueix de forma automàtica els GIF i vídeos",
|
||||
"Enable automatic language detection for syntax highlighting": "Activa la detecció automàtica d'idiomes per al ressaltat de sintaxi",
|
||||
"Automatically replace plain text Emoji": "Substitueix automàticament Emoji de text pla",
|
||||
@@ -204,7 +204,7 @@
|
||||
"Cannot add any more widgets": "No s'ha pogut afegir cap més giny",
|
||||
"The maximum permitted number of widgets have already been added to this room.": "Ja s'han afegit el màxim de ginys permesos en aquesta sala.",
|
||||
"Drop File Here": "Deixeu anar un fitxer aquí",
|
||||
"Drop file here to upload": "Deixeu anar un arxiu aquí per pujar-lo",
|
||||
"Drop file here to upload": "Deixa anar el fitxer aquí per pujar-lo",
|
||||
" (unsupported)": " (incompatible)",
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Uneix-te com <voiceText>voice</voiceText> o <videoText>video</videoText>.",
|
||||
"Ongoing conference call%(supportedText)s.": "Trucada de conferència en curs %(supportedText)s.",
|
||||
@@ -225,10 +225,10 @@
|
||||
"Ban this user?": "Voleu expulsar a aquest usuari?",
|
||||
"Failed to ban user": "No s'ha pogut expulsar l'usuari",
|
||||
"Failed to mute user": "No s'ha pogut silenciar l'usuari",
|
||||
"Failed to change power level": "No s'ha pogut canviar el nivell de poders",
|
||||
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "No podreu desfer aquest canvi ja que estareu baixant de grau de privilegis. Només un altre usuari amb més privilegis podrà fer que els recupereu.",
|
||||
"Are you sure?": "Esteu segur?",
|
||||
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "No podreu desfer aquesta acció ja que esteu donant al usuari el mateix nivell de privilegi que el vostre.",
|
||||
"Failed to change power level": "No s'ha pogut canviar el nivell d'autoritat",
|
||||
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "No podràs desfer aquest canvi ja que t'estàs baixant de rang, si ets l'últim usuari de la sala amb privilegis, et serà impossible recuperar-los.",
|
||||
"Are you sure?": "Estàs segur?",
|
||||
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "No podràs desfer aquest canvi ja que estàs donant a l'usuari el mateix nivell d'autoritat que el teu.",
|
||||
"Unignore": "Deixa de ignorar",
|
||||
"Ignore": "Ignora",
|
||||
"Jump to read receipt": "Vés a l'últim missatge llegit",
|
||||
@@ -240,7 +240,7 @@
|
||||
"and %(count)s others...|one": "i un altre...",
|
||||
"Invited": "Convidat",
|
||||
"Filter room members": "Filtra els membres de la sala",
|
||||
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (poder %(powerLevelNumber)s)",
|
||||
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (autoritat %(powerLevelNumber)s)",
|
||||
"Attachment": "Adjunt",
|
||||
"Hangup": "Penja",
|
||||
"Voice call": "Trucada de veu",
|
||||
@@ -248,11 +248,11 @@
|
||||
"Upload file": "Puja un fitxer",
|
||||
"Send an encrypted reply…": "Envia una resposta xifrada…",
|
||||
"Send an encrypted message…": "Envia un missatge xifrat…",
|
||||
"You do not have permission to post to this room": "No teniu el permís per escriure en aquesta sala",
|
||||
"Server error": "S'ha produït un error al servidor",
|
||||
"Mirror local video feed": "Mostra el vídeo local com un mirall",
|
||||
"You do not have permission to post to this room": "No tens permís per enviar res en aquesta sala",
|
||||
"Server error": "Error de servidor",
|
||||
"Mirror local video feed": "Remet el flux de vídeo local",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "El servidor no està disponible, està sobrecarregat o alguna altra cosa no ha funcionat correctament.",
|
||||
"Command error": "S'ha produït un error en l'ordre",
|
||||
"Command error": "Error en l'ordre",
|
||||
"Jump to message": "Salta al missatge",
|
||||
"No pinned messages.": "No hi ha cap missatge fixat.",
|
||||
"Loading...": "S'està carregant...",
|
||||
@@ -263,15 +263,15 @@
|
||||
"%(duration)sd": "%(duration)sd",
|
||||
"Online for %(duration)s": "En línia durant %(duration)s",
|
||||
"Idle for %(duration)s": "Inactiu durant %(duration)s",
|
||||
"Offline for %(duration)s": "Desconnectat durant %(duration)s",
|
||||
"Offline for %(duration)s": "Fora de línia durant %(duration)s",
|
||||
"Unknown for %(duration)s": "Desconegut durant %(duration)s",
|
||||
"Online": "Conectat",
|
||||
"Online": "En línia",
|
||||
"Idle": "Inactiu",
|
||||
"Offline": "Desconnectat",
|
||||
"Offline": "Fora de línia",
|
||||
"Unknown": "Desconegut",
|
||||
"Replying": "S'està contestant",
|
||||
"Seen by %(userName)s at %(dateTime)s": "Vist per %(userName)s a les %(dateTime)s",
|
||||
"No rooms to show": "No hi ha cap sala per a mostrar",
|
||||
"No rooms to show": "No hi ha sales per mostrar",
|
||||
"Unnamed room": "Sala sense nom",
|
||||
"Save": "Desa",
|
||||
"(~%(count)s results)|other": "(~%(count)s resultats)",
|
||||
@@ -299,7 +299,7 @@
|
||||
"Who can access this room?": "Qui pot entrar a aquesta sala?",
|
||||
"Only people who have been invited": "Només les persones que hi hagin sigut convidades",
|
||||
"Anyone who knows the room's link, apart from guests": "Qualsevol que conegui l'enllaç de la sala, excepte usuaris d'altres xarxes",
|
||||
"Publish this room to the public in %(domain)s's room directory?": "Voleu que es publiqui aquesta sala al directori de sales públiques de %(domain)s?",
|
||||
"Publish this room to the public in %(domain)s's room directory?": "Vols publicar aquesta sala al directori de sales públiques de %(domain)s?",
|
||||
"Who can read history?": "Qui pot llegir l'historial?",
|
||||
"Anyone": "Qualsevol",
|
||||
"Members only (since the point in time of selecting this option)": "Només els membres (a partir del punt en què seleccioneu aquesta opció)",
|
||||
@@ -313,30 +313,30 @@
|
||||
"This room has no local addresses": "Aquesta sala no té adreces locals",
|
||||
"Invalid community ID": "L'ID de la comunitat no és vàlid",
|
||||
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' no és un ID de comunitat vàlid",
|
||||
"New community ID (e.g. +foo:%(localDomain)s)": "Nou ID de comunitat (per exemple +foo:%(localDomain)s)",
|
||||
"New community ID (e.g. +foo:%(localDomain)s)": "Nou ID de comunitat (p.e. +foo:%(localDomain)s)",
|
||||
"You have <a>enabled</a> URL previews by default.": "Heu <a>habilitat</a> les previsualitzacions per defecte dels URL.",
|
||||
"You have <a>disabled</a> URL previews by default.": "Heu <a>inhabilitat</a> les previsualitzacions per defecte dels URL.",
|
||||
"URL previews are enabled by default for participants in this room.": "Les previsualitzacions dels URL estan habilitades per defecte per als membres d'aquesta sala.",
|
||||
"URL previews are disabled by default for participants in this room.": "Les previsualitzacions dels URL estan inhabilitades per defecte per als membres d'aquesta sala.",
|
||||
"URL Previews": "Previsualitzacions dels URL",
|
||||
"Error decrypting audio": "S'ha produït un error mentre es desxifrava l'àudio",
|
||||
"Error decrypting attachment": "S'ha produït un error en desxifrar el fitxer adjunt",
|
||||
"Error decrypting audio": "Error desxifrant àudio",
|
||||
"Error decrypting attachment": "Error desxifrant fitxer adjunt",
|
||||
"Decrypt %(text)s": "Desxifra %(text)s",
|
||||
"Download %(text)s": "Baixa %(text)s",
|
||||
"Invalid file%(extra)s": "Fitxer invàlid%(extra)s",
|
||||
"Error decrypting image": "S'ha produït un error en desxifrar la imatge",
|
||||
"Error decrypting video": "S'ha produït un error en desxifrar el vídeo",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s ha canviat el seu avatar per a la sala %(roomName)s",
|
||||
"Error decrypting image": "Error desxifrant imatge",
|
||||
"Error decrypting video": "Error desxifrant video",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s ha canviat l'avatar de %(roomName)s",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s ha eliminat l'avatar de la sala.",
|
||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s ha canviat l'avatar de la sala per aquest <img/>",
|
||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s ha canviat l'avatar de la sala a <img/>",
|
||||
"Copied!": "Copiat!",
|
||||
"Failed to copy": "No s'ha pogut copiar",
|
||||
"Add an Integration": "Afegeix una integració",
|
||||
"An email has been sent to %(emailAddress)s": "S'ha enviat un correu electrònic a %(emailAddress)s",
|
||||
"Please check your email to continue registration.": "Reviseu el vostre correu electrònic per a poder continuar amb el registre.",
|
||||
"Token incorrect": "El testimoni és incorrecte",
|
||||
"Token incorrect": "Token incorrecte",
|
||||
"A text message has been sent to %(msisdn)s": "S'ha enviat un missatge de text a %(msisdn)s",
|
||||
"Please enter the code it contains:": "Introduïu el codi que conté:",
|
||||
"Please enter the code it contains:": "Introdueix el codi que conté:",
|
||||
"Start authentication": "Inicia l'autenticació",
|
||||
"Sign in with": "Inicieu sessió amb",
|
||||
"Email address": "Correu electrònic",
|
||||
@@ -349,17 +349,17 @@
|
||||
"Failed to remove user from community": "No s'ha pogut treure l'usuari de la comunitat",
|
||||
"Filter community members": "Filtra els membres de la comunitat",
|
||||
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Esteu segur que voleu treure l'usuari '%(roomName)s' del grup %(groupId)s?",
|
||||
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
||||
"<a>In reply to</a> <pill>": "<a>En resposta a</a> <pill>",
|
||||
"Removing a room from the community will also remove it from the community page.": "L'eliminació d'una sala de la comunitat també l'eliminarà de la pàgina de la comunitat.",
|
||||
"Failed to remove room from community": "No s'ha pogut eliminar la sala de la comunitat",
|
||||
"Failed to remove '%(roomName)s' from %(groupId)s": "No s'ha pogut treure la sala '%(roomName)s' de la comunitat %(groupId)s",
|
||||
"Failed to remove '%(roomName)s' from %(groupId)s": "No s'ha pogut eliminar '%(roomName)s' de %(groupId)s",
|
||||
"Something went wrong!": "Alguna cosa ha anat malament!",
|
||||
"The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "No s'ha pogut actualitzar la visibilitat de la sala '%(roomName)s' de la comunitat %(groupId)s.",
|
||||
"Visibility in Room List": "Visibilitat a la llista de les sales",
|
||||
"Visible to everyone": "Visible per a tothom",
|
||||
"Only visible to community members": "Només visible per als membres de la comunitat",
|
||||
"Filter community rooms": "Filtra sales de comunitats",
|
||||
"Something went wrong when trying to get your communities.": "S'ha produït un error en intentar obtenir les vostres comunitats.",
|
||||
"Something went wrong when trying to get your communities.": "Alguna cosa ha anat malament mentre s'intentaven obtenir les comunitats.",
|
||||
"You're not currently a member of any communities.": "Actualment no sou membre de cap comunitat.",
|
||||
"Unknown Address": "Adreça desconeguda",
|
||||
"Allow": "Permetre",
|
||||
@@ -372,12 +372,12 @@
|
||||
"Home": "Inici",
|
||||
"Manage Integrations": "Gestiona les integracions",
|
||||
"%(nameList)s %(transitionList)s": "%(transitionList)s%(nameList)s",
|
||||
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s han entrat",
|
||||
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s s'hi han unit",
|
||||
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s s'ha unit",
|
||||
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s han sortit",
|
||||
"%(oneUser)sleft %(count)s times|one": "%(oneUser)s ha sortit",
|
||||
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s s'han unit i han sortit %(count)s vegades",
|
||||
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s s'han unit i han sortit",
|
||||
"%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s s'hi han unit i han sortit %(count)s vegades",
|
||||
"%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s s'hi han unit i han sortit",
|
||||
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s ha entrat i ha sortit %(count)s vegades",
|
||||
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s ha entrat i ha sortit",
|
||||
"%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s han sortit i han tornat a entrar %(count)s vegades",
|
||||
@@ -388,10 +388,10 @@
|
||||
"%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s han rebutjat les seves invitacions",
|
||||
"%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s ha rebutjat la seva invitació %(count)s vegades",
|
||||
"%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s ha rebutjat la seva invitació",
|
||||
"%(severalUsers)shad their invitations withdrawn %(count)s times|other": "a %(severalUsers)s els hi han retirat les seves invitacions %(count)s vegades",
|
||||
"%(severalUsers)shad their invitations withdrawn %(count)s times|one": "a %(severalUsers)s els hi han retirat les seves invitacions",
|
||||
"%(oneUser)shad their invitation withdrawn %(count)s times|other": "a %(oneUser)s li han retirat la seva invitació %(count)s vegades",
|
||||
"%(oneUser)shad their invitation withdrawn %(count)s times|one": "a %(oneUser)s li han retirat la seva invitació",
|
||||
"%(severalUsers)shad their invitations withdrawn %(count)s times|other": "S'han retirat les invitacions de %(severalUsers)s %(count)s vegades",
|
||||
"%(severalUsers)shad their invitations withdrawn %(count)s times|one": "S'han retirat les invitacions de %(severalUsers)s",
|
||||
"%(oneUser)shad their invitation withdrawn %(count)s times|other": "S'ha retirat la invitació de %(oneUser)s %(count)s vegades",
|
||||
"%(oneUser)shad their invitation withdrawn %(count)s times|one": "S'ha retirat la invitació de %(oneUser)s",
|
||||
"were invited %(count)s times|other": "a sigut invitat %(count)s vegades",
|
||||
"were invited %(count)s times|one": "han sigut convidats",
|
||||
"was invited %(count)s times|other": "ha sigut convidat %(count)s vegades",
|
||||
@@ -410,7 +410,7 @@
|
||||
"was kicked %(count)s times|one": "l'han fet fora",
|
||||
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s han canviat el seu nom %(count)s vegades",
|
||||
"%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s han canviat el seu nom",
|
||||
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s han canviat el seu nom %(count)s vegades",
|
||||
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s ha canviat el seu nom %(count)s vegades",
|
||||
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s ha canviat el seu nom",
|
||||
"%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s han canviat el seu avatar %(count)s vegades",
|
||||
"%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s han canviat el seu avatar",
|
||||
@@ -426,12 +426,12 @@
|
||||
"ex. @bob:example.com": "per exemple @carles:exemple.cat",
|
||||
"Add User": "Afegeix un usuari",
|
||||
"Matrix ID": "ID de Matrix",
|
||||
"Matrix Room ID": "ID de sala de Matrix",
|
||||
"Matrix Room ID": "ID de la sala de Matrix",
|
||||
"email address": "correu electrònic",
|
||||
"Try using one of the following valid address types: %(validTypesList)s.": "Proveu d'utilitzar un dels següents tipus d'adreça vàlids: %(validTypesList)s.",
|
||||
"You have entered an invalid address.": "No heu introduït una adreça vàlida.",
|
||||
"Confirm Removal": "Confirmeu l'eliminació",
|
||||
"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.": "Esteu segur que voleu eliminar (suprimir) aquest esdeveniment? Tingueu en compte que si suprimiu un nom sala o si feu un canvi de tema, desfaria el canvi.",
|
||||
"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.": "Estàs segur que vols eliminar (suprimir) aquest esdeveniment? Tingues en compte que, si suprimeixes un nom de sala o es canvia el tema, podria ser que es revertís.",
|
||||
"This room is not showing flair for any communities": "Aquesta sala no mostra talent per a cap comunitat",
|
||||
"Flair": "Talents",
|
||||
"Showing flair for these communities:": "Mostra els talents d'aquestes comunitats:",
|
||||
@@ -442,7 +442,7 @@
|
||||
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s ha sortit %(count)s vegades",
|
||||
"Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Les ID de les comunitats només poden contendre caràcters a-z, 0-9, o '=_-./'",
|
||||
"Community IDs cannot be empty.": "Les ID de les comunitats no poden estar buides.",
|
||||
"Something went wrong whilst creating your community": "S'ha produït un error al crear la vostra comunitat",
|
||||
"Something went wrong whilst creating your community": "S'ha produït un error mentre es creava la comunitat",
|
||||
"Create Community": "Crea una comunitat",
|
||||
"Community Name": "Nom de la comunitat",
|
||||
"Example": "Exemple",
|
||||
@@ -450,13 +450,13 @@
|
||||
"example": "exemple",
|
||||
"Create": "Crea",
|
||||
"Create Room": "Crea una sala",
|
||||
"Unknown error": "S'ha produït un error desconegut",
|
||||
"Unknown error": "Error desconegut",
|
||||
"Incorrect password": "Contrasenya incorrecta",
|
||||
"Deactivate Account": "Desactivar el compte",
|
||||
"An error has occurred.": "S'ha produït un error.",
|
||||
"Unable to restore session": "No s'ha pogut restaurar la sessió",
|
||||
"Invalid Email Address": "El correu electrònic no és vàlid",
|
||||
"This doesn't appear to be a valid email address": "Aquest no sembla ser un correu electrònic vàlid",
|
||||
"This doesn't appear to be a valid email address": "Sembla que aquest correu electrònic no és vàlid",
|
||||
"Verification Pending": "Verificació pendent",
|
||||
"Please check your email and click on the link it contains. Once this is done, click continue.": "Reviseu el vostre correu electrònic i feu clic a l'enllaç que conté. Un cop fet això, feu clic a Continua.",
|
||||
"Unable to add email address": "No s'ha pogut afegir el correu electrònic",
|
||||
@@ -475,8 +475,8 @@
|
||||
"Public Chat": "Xat públic",
|
||||
"Custom": "Personalitzat",
|
||||
"Name": "Nom",
|
||||
"You must <a>register</a> to use this functionality": "Heu de <a>register</a> per utilitzar aquesta funcionalitat",
|
||||
"You must join the room to see its files": "Heu d'entrar a la sala per poder-ne veure els fitxers",
|
||||
"You must <a>register</a> to use this functionality": "Per poder utilitzar aquesta funcionalitat has de <a>registrar-te</a>",
|
||||
"You must join the room to see its files": "Per poder veure els fitxers de la sala t'hi has d'unir",
|
||||
"There are no visible files in this room": "No hi ha fitxers visibles en aquesta sala",
|
||||
"<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 use 'img' tags\n</p>\n": "<h1>Aquest és l'HTML per a la pàgina de la vostra comunitat</h1>\n<p>\n Utilitzeu la descripció llarga per a presentar la comunitat a nous membres,\n o per afegir-hi <a href=\"foo\">enlaços</a> d'interès. \n</p>\n<p>\n També podeu utilitzar etiquetes 'img'.\n</p>\n",
|
||||
"Add rooms to the community summary": "Afegiu sales al resum de la comunitat",
|
||||
@@ -499,15 +499,15 @@
|
||||
"Leave Community": "Abandona la comunitat",
|
||||
"Leave %(groupName)s?": "Voleu sortir de la comunitat %(groupName)s?",
|
||||
"Leave": "Surt",
|
||||
"Community Settings": "Paràmetres de la comunitat",
|
||||
"Community Settings": "Configuració de comunitat",
|
||||
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Aquestes sales es mostren a la pàgina de la comunitat als seus membres i poden entrar-hi fent clic sobre elles.",
|
||||
"Featured Rooms:": "Sales destacades:",
|
||||
"Featured Users:": "Usuaris destacats:",
|
||||
"%(inviter)s has invited you to join this community": "%(inviter)s vos convida a unir-vos a aquesta comunitat",
|
||||
"You are an administrator of this community": "Sou un administrador d'aquesta comunitat",
|
||||
"You are a member of this community": "Sou un membre d'aquesta comunitat",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Esteu a punt de ser portat a un lloc de tercers perquè pugui autenticar-se amb el vostre compte per utilitzar-lo amb %(integrationsUrl)s. Voleu continuar?",
|
||||
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "La vostra comunitat no té una descripció llarga, una pàgina HTML per mostrar als membres de la comunitat. <br />Feu clic aquí per obrir la configuració i donar-ne una!",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Estàs a punt de ser redirigit a una web de tercers per autenticar el teu compte i poder ser utilitzat amb %(integrationsUrl)s. Vols continuar?",
|
||||
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "La teva comunitat no té cap descripció llarga (una pàgina HTML per mostrar als membres de la comunitat).<br />Clica aquí per obrir la configuració i crear-ne una!",
|
||||
"Long Description (HTML)": "Descripció llarga (HTML)",
|
||||
"Description": "Descripció",
|
||||
"Community %(groupId)s not found": "No s'ha pogut trobar la comunitat %(groupId)s",
|
||||
@@ -522,7 +522,7 @@
|
||||
"Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "S'han detectat dades d'una versió antiga del %(brand)s. Això haurà provocat que el xifratge d'extrem a extrem no funcioni correctament a la versió anterior. Els missatges xifrats d'extrem a extrem que s'han intercanviat recentment mentre s'utilitzava la versió anterior no es poden desxifrar en aquesta versió. També pot provocar que els missatges intercanviats amb aquesta versió fallin. Si teniu problemes, sortiu de la sessió i torneu a entrar-hi. Per poder llegir l'historial dels missatges xifrats, exporteu i torneu a importar les vostres claus.",
|
||||
"Logout": "Surt",
|
||||
"Your Communities": "Les teves comunitats",
|
||||
"Error whilst fetching joined communities": "S'ha produït un error en buscar comunitats unides",
|
||||
"Error whilst fetching joined communities": "Error en l'obtenció de comunitats unides",
|
||||
"Create a new community": "Crea una comunitat nova",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Crea una comunitat per agrupar usuaris i sales! Creeu una pàgina d'inici personalitzada per definir el vostre espai a l'univers Matrix.",
|
||||
"You have no visible notifications": "No teniu cap notificació visible",
|
||||
@@ -556,7 +556,7 @@
|
||||
"Sign out": "Tanca la sessió",
|
||||
"Import E2E room keys": "Importar claus E2E de sala",
|
||||
"Cryptography": "Criptografia",
|
||||
"Labs": "Laboraroris",
|
||||
"Labs": "Laboratoris",
|
||||
"%(brand)s version:": "Versió de %(brand)s:",
|
||||
"olm version:": "Versió d'olm:",
|
||||
"Incorrect username and/or password.": "Usuari i/o contrasenya incorrectes.",
|
||||
@@ -568,23 +568,23 @@
|
||||
"Export": "Exporta",
|
||||
"Import room keys": "Importa les claus de la sala",
|
||||
"Import": "Importa",
|
||||
"The version of %(brand)s": "La versió del %(brand)s",
|
||||
"The version of %(brand)s": "La versió de %(brand)s",
|
||||
"Email": "Correu electrònic",
|
||||
"I have verified my email address": "He verificat l'adreça de correu electrònic",
|
||||
"Send Reset Email": "Envia email de reinici",
|
||||
"Your homeserver's URL": "L'URL del vostre servidor personal",
|
||||
"Your homeserver's URL": "L'URL del teu servidor propi",
|
||||
"Analytics": "Analítiques",
|
||||
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ha canviat el seu nom visible a %(displayName)s.",
|
||||
"Identity Server is": "El servidor d'identitat és",
|
||||
"Submit debug logs": "Enviar logs de depuració",
|
||||
"The platform you're on": "La plataforma a la que estàs",
|
||||
"Your language of choice": "El teu idioma preferit",
|
||||
"The platform you're on": "La plataforma a la que et trobes",
|
||||
"Your language of choice": "El teu idioma desitjat",
|
||||
"Which officially provided instance you are using, if any": "Quina instància oficial estàs utilitzant, si escau",
|
||||
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Si esteu utilitzant el mode Richtext del Rich Text Editor o no",
|
||||
"The information being sent to us to help make %(brand)s better includes:": "La informació enviada a %(brand)s per ajudar-nos a millorar inclou:",
|
||||
"Fetching third party location failed": "S'ha produït un error en obtenir la ubicació de tercers",
|
||||
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Si estàs utilitzant, o no, el mode Richtext de Rich Text Editor",
|
||||
"The information being sent to us to help make %(brand)s better includes:": "La informació que s'envia a %(brand)s per ajudar-nos a millorar inclou:",
|
||||
"Fetching third party location failed": "Ha fallat l'obtenció de la ubicació de tercers",
|
||||
"Send Account Data": "Envia les dades del compte",
|
||||
"Advanced notification settings": "Paràmetres avançats de notificacions",
|
||||
"Advanced notification settings": "Configuració avançada de notificacions",
|
||||
"Uploading report": "S'està enviant l'informe",
|
||||
"Sunday": "Diumenge",
|
||||
"Failed to add tag %(tagName)s to room": "No s'ha pogut afegir l'etiqueta %(tagName)s a la sala",
|
||||
@@ -612,14 +612,14 @@
|
||||
"Messages containing my display name": "Missatges que contenen el meu nom visible",
|
||||
"Messages in one-to-one chats": "Missatges en xats un a un",
|
||||
"Unavailable": "No disponible",
|
||||
"Error saving email notification preferences": "No s'han pogut desar les preferències de les notificacions a causa d'un error",
|
||||
"Error saving email notification preferences": "Error desant preferències de notificacions de correu electrònic",
|
||||
"View Decrypted Source": "Mostra el codi desxifrat",
|
||||
"Failed to update keywords": "No s'han pogut actualitzar les paraules clau",
|
||||
"remove %(name)s from the directory.": "elimina %(name)s del directori.",
|
||||
"Notifications on the following keywords follow rules which can’t be displayed here:": "Les notificacions sobre les següents paraules clau segueixen regles que no es poden mostrar aquí:",
|
||||
"Please set a password!": "Si us plau, establiu una contrasenya",
|
||||
"You have successfully set a password!": "Heu establert correctament la contrasenya",
|
||||
"An error occurred whilst saving your email notification preferences.": "S'ha produït un error en desar les vostres preferències de notificació per correu electrònic.",
|
||||
"An error occurred whilst saving your email notification preferences.": "S'ha produït un error mentre es desaven les teves preferències de notificació de correu electrònic.",
|
||||
"Explore Room State": "Esbrina els estats de les sales",
|
||||
"Source URL": "URL origen",
|
||||
"Messages sent by bot": "Missatges enviats pel bot",
|
||||
@@ -653,7 +653,7 @@
|
||||
"Enable them now": "Habilita-ho ara",
|
||||
"Toolbox": "Caixa d'eines",
|
||||
"Collecting logs": "S'estan recopilant els registres",
|
||||
"You must specify an event type!": "Heu d'especificar un tipus d'esdeveniment",
|
||||
"You must specify an event type!": "Has d'especificar un tipus d'esdeveniment!",
|
||||
"(HTTP status %(httpStatus)s)": "(Estat de l´HTTP %(httpStatus)s)",
|
||||
"All Rooms": "Totes les sales",
|
||||
"State Key": "Clau d'estat",
|
||||
@@ -668,7 +668,7 @@
|
||||
"Notify me for anything else": "Notifica'm per a qualsevol altra cosa",
|
||||
"View Source": "Mostra el codi",
|
||||
"Keywords": "Paraules clau",
|
||||
"Can't update user notification settings": "No es poden actualitzar els paràmetres de les notificacions de l'usuari",
|
||||
"Can't update user notification settings": "No es pot actualitzar la configuració de notificacions d'usuari",
|
||||
"Notify for all other messages/rooms": "Notifica per a tots els altres missatges o sales",
|
||||
"Unable to look up room ID from server": "No s'ha pogut cercar l'ID de la sala en el servidor",
|
||||
"Couldn't find a matching Matrix room": "No s'ha pogut trobar una sala de Matrix que coincideixi",
|
||||
@@ -700,44 +700,44 @@
|
||||
"Event Type": "Tipus d'esdeveniment",
|
||||
"Download this file": "Descarrega aquest fitxer",
|
||||
"Pin Message": "Enganxa el missatge",
|
||||
"Failed to change settings": "No s'han pogut modificar els paràmetres",
|
||||
"Failed to change settings": "No s'ha pogut canviar la configuració",
|
||||
"View Community": "Mira la communitat",
|
||||
"Event sent!": "S'ha enviat l'esdeveniment",
|
||||
"Event sent!": "Esdeveniment enviat!",
|
||||
"Event Content": "Contingut de l'esdeveniment",
|
||||
"Thank you!": "Gràcies!",
|
||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Amb el vostre navegador actual, l'aparença de l'aplicació pot ser completament incorrecta i algunes o totes les funcions poden no funcionar correctament. Si voleu provar-ho de totes maneres, podeu continuar, però esteu sols pel que fa als problemes que pugueu trobar!",
|
||||
"Checking for an update...": "Comprovant si hi ha actualitzacions...",
|
||||
"e.g. %(exampleValue)s": "p. ex. %(exampleValue)s",
|
||||
"Every page you use in the app": "Cada pàgina que utilitzeu a l'aplicació",
|
||||
"e.g. <CurrentPageURL>": "p. ex. <CurrentPageURL>",
|
||||
"Your device resolution": "La resolució del vostre dispositiu",
|
||||
"e.g. %(exampleValue)s": "p.e. %(exampleValue)s",
|
||||
"Every page you use in the app": "Cada pàgina que utilitzes a l'aplicació",
|
||||
"e.g. <CurrentPageURL>": "p.e. <CurrentPageURL>",
|
||||
"Your device resolution": "La resolució del teu dispositiu",
|
||||
"Show Stickers": "Mostra els adhesius",
|
||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quan aquesta pàgina contingui informació identificable, com per exemple una sala, usuari o ID de grup, aquestes dades se suprimeixen abans d'enviar-se al servidor.",
|
||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quan aquesta pàgina contingui informació d'identificació, com per exemple una sala, usuari o ID de grup, aquestes dades s'eliminen abans d'enviar-se al servidor.",
|
||||
"Call in Progress": "Trucada en curs",
|
||||
"A call is currently being placed!": "S'està fent una trucada en aquest moment!",
|
||||
"A call is currently being placed!": "En aquest moment s'està realitzant una trucada!",
|
||||
"A call is already in progress!": "Ja hi ha una trucada en curs!",
|
||||
"Permission Required": "Permís requerit",
|
||||
"You do not have permission to start a conference call in this room": "No teniu permís per iniciar una trucada de conferència en aquesta sala",
|
||||
"Unable to load! Check your network connectivity and try again.": "No s'ha pogut carregar! Comproveu la vostra connectivitat i torneu-ho a provar.",
|
||||
"Failed to invite users to the room:": "No s'ha pogut convidar els usuaris a aquesta sala:",
|
||||
"Missing roomId.": "Manca l'ID de la sala.",
|
||||
"Permission Required": "Es necessita permís",
|
||||
"You do not have permission to start a conference call in this room": "No tens permís per iniciar una conferència telefònica en aquesta sala",
|
||||
"Unable to load! Check your network connectivity and try again.": "No s'ha pogut carregar! Comprova la connectivitat de xarxa i torna-ho a intentar.",
|
||||
"Failed to invite users to the room:": "No s'han pogut convidar els usuaris a la sala:",
|
||||
"Missing roomId.": "Falta l'ID de sala.",
|
||||
"Searches DuckDuckGo for results": "Cerca al DuckDuckGo els resultats",
|
||||
"Changes your display nickname": "Canvia el vostre malnom",
|
||||
"Invites user with given id to current room": "Convida l'usuari amb l'id donat a la sala actual",
|
||||
"Kicks user with given id": "Expulsa l'usuari amb l'id donat",
|
||||
"Bans user with given id": "Bandeja l'usuari amb l'id donat",
|
||||
"Changes your display nickname": "Canvia el teu àlies de visualització",
|
||||
"Invites user with given id to current room": "Convida a la sala actual l'usuari amb l'ID indicat",
|
||||
"Kicks user with given id": "Expulsa l'usuari amb l'ID indicat",
|
||||
"Bans user with given id": "Bandeja l'usuari amb l'ID indicat",
|
||||
"Ignores a user, hiding their messages from you": "Ignora un usuari, amagant-te els seus missatges",
|
||||
"Stops ignoring a user, showing their messages going forward": "Deixa d'ignorar un usuari, mostrant els seus missatges ara en avant",
|
||||
"Stops ignoring a user, showing their messages going forward": "Deixa d'ignorar un usuari, i mostra els seus missatges a partir d'ara",
|
||||
"Define the power level of a user": "Defineix el nivell d'autoritat d'un usuari",
|
||||
"Deops user with given id": "Degrada l'usuari amb l'id donat",
|
||||
"Opens the Developer Tools dialog": "Obre el diàleg d'Eines del desenvolupador",
|
||||
"Displays action": "Mostra l'acció",
|
||||
"Whether or not you're logged in (we don't record your username)": "Si heu iniciat sessió o no (no desem el vostre usuari)",
|
||||
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "El fitxer %(fileName)s supera el límit de pujades del servidor",
|
||||
"Whether or not you're logged in (we don't record your username)": "Si has iniciat sessió o no (no registrem el teu nom d'usuari)",
|
||||
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "El fitxer %(fileName)s supera la mida màxima permesa per a pujades d'aquest servidor",
|
||||
"Upgrades a room to a new version": "Actualitza la sala a una versió nova",
|
||||
"Gets or sets the room topic": "Consulta o canvia el tema de la sala",
|
||||
"This room has no topic.": "Aquesta sala no te tema.",
|
||||
"Sets the room name": "Canvia el nom de la sala",
|
||||
"Gets or sets the room topic": "Obté o estableix el tema de la sala",
|
||||
"This room has no topic.": "Aquesta sala no té tema.",
|
||||
"Sets the room name": "Estableix el nom de la sala",
|
||||
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s ha actualitzat aquesta sala.",
|
||||
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s ha fet la sala pública a tothom qui conegui l'adreça.",
|
||||
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s ha limitat la sala als convidats.",
|
||||
@@ -778,7 +778,7 @@
|
||||
"Common names and surnames are easy to guess": "Noms i cognoms comuns són fàcils d'esbrinar",
|
||||
"Straight rows of keys are easy to guess": "Fileres seguides de tecles són fàcils d'esbrinar",
|
||||
"Short keyboard patterns are easy to guess": "Patrons curts de teclat són fàcils d'esbrinar",
|
||||
"There was an error joining the room": "Hi ha hagut un error en entrar a la sala",
|
||||
"There was an error joining the room": "Hi ha hagut un error unint-se a la sala",
|
||||
"Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "No s'ha trobat el perfil pels IDs de Matrix següents, els voleu convidar igualment?",
|
||||
"Invite anyway and never warn me again": "Convidar igualment i no avisar-me de nou",
|
||||
"Invite anyway": "Convidar igualment",
|
||||
@@ -811,5 +811,142 @@
|
||||
"Theme": "Tema",
|
||||
"Phone Number": "Número de telèfon",
|
||||
"Help": "Ajuda",
|
||||
"Send typing notifications": "Envia notificacions d'escriptura"
|
||||
"Send typing notifications": "Envia notificacions d'escriptura",
|
||||
"Delete the room address %(alias)s and remove %(name)s from the directory?": "Vols suprimir l'adreça de la sala %(alias)s i eliminar %(name)s del directori?",
|
||||
"We encountered an error trying to restore your previous session.": "Hem trobat un error en intentar recuperar la teva sessió prèvia.",
|
||||
"There was an error updating your community. The server is unable to process your request.": "S'ha produït un error en actualitzar la comunitat. El servidor no ha pogut processar la petició.",
|
||||
"There was an error creating your community. The name may be taken or the server is unable to process your request.": "S'ha produït un error en crear la comunitat. Potser el nom ja existeix o el servidor no ha pogut processar la petició.",
|
||||
"An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "S'ha retornat un error (%(errcode)s) mentre s'intentava validar la invitació. Pots provar a informar d'això a un administrador de sala.",
|
||||
"Error: Problem communicating with the given homeserver.": "Error: problema comunicant-se amb el servidor local proporcionat.",
|
||||
"Upload Error": "Error de pujada",
|
||||
"A connection error occurred while trying to contact the server.": "S'ha produït un error de connexió mentre s'intentava connectar al servidor.",
|
||||
"%(brand)s encountered an error during upload of:": "%(brand)s ha trobat un error durant la pujada de:",
|
||||
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "S'ha produït un error en actualitzar l'adreça principal de la sala. Pot ser que el servidor no ho permeti o que s'hagi produït un error temporal.",
|
||||
"Error upgrading room": "Error actualitzant sala",
|
||||
"Error unsubscribing from list": "Error en cancel·lar subscripció de la llista",
|
||||
"Error changing power level requirement": "Error en canviar requisit del nivell d'autoritat",
|
||||
"Error changing power level": "Error en canviar nivell d'autoritat",
|
||||
"Error updating main address": "Error actualitzant adreça principal",
|
||||
"Error creating address": "Error creant adreça",
|
||||
"Error removing address": "Error eliminant adreça",
|
||||
"There was an error removing that address. It may no longer exist or a temporary error occurred.": "S'ha produït un error en eliminar l'adreça. Pot ser que ja no existeixi o que s'hagi produït un error temporal.",
|
||||
"There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "S'ha produït un error en crear l'adreça. Pot ser que el servidor no ho permeti o que s'hagi produït un error temporal.",
|
||||
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "S'ha produït un error en actualitzar l'adreça alternativa de la sala. Pot ser que el servidor no ho permeti o que s'hagi produït un error temporal.",
|
||||
"%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "S'ha retornat %(errcode)s mentre s'intentava accedir a la sala. Si creus que aquest missatge és un error, <issueLink>envia un informe d'error</issueLink>.",
|
||||
"Error removing ignored user/server": "Error eliminant usuari/servidor ignorat",
|
||||
"Error subscribing to list": "Error subscrivint-se a la llista",
|
||||
"Error adding ignored user/server": "Error afegint usuari/servidor ignorat",
|
||||
"Error downloading theme information.": "Error baixant informació de tema.",
|
||||
"Unexpected server error trying to leave the room": "Error de servidor inesperat intentant sortir de la sala",
|
||||
"Error leaving room": "Error sortint de la sala",
|
||||
"Unexpected error resolving identity server configuration": "Error inesperat resolent la configuració del servidor d'identitat",
|
||||
"Unexpected error resolving homeserver configuration": "Error inesperat resolent la configuració del servidor local",
|
||||
"(an error occurred)": "(s'ha produït un error)",
|
||||
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Els gestors d'integracions reben dades de configuració i poden modificar ginys, enviar invitacions a sales i establir nivells d'autoritat en nom teu.",
|
||||
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "S'ha produït un error en canviar els requisits del nivell d'autoritat de la sala. Assegura't que tens suficients permisos i torna-ho a provar.",
|
||||
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "S'ha produït un error en canviar el nivell d'autoritat de l'usuari. Assegura't que tens suficients permisos i torna-ho a provar.",
|
||||
"Power level": "Nivell d'autoritat",
|
||||
"Unbans user with given ID": "Desbandeja l'usuari amb l'ID indicat",
|
||||
"Unrecognised room address:": "Adreça de sala no reconeguda:",
|
||||
"Joins room with given address": "S'uneix a la sala amb l'adreça indicada",
|
||||
"Use an identity server": "Utilitza un servidor d'identitat",
|
||||
"Failed to set topic": "No s'ha pogut establir el tema",
|
||||
"Changes your avatar in all rooms": "Canvia el teu avatar en totes les sales",
|
||||
"Changes your avatar in this current room only": "Canvia el teu avatar només en aquesta sala actual",
|
||||
"Changes the avatar of the current room": "Canvia l'avatar de la sala actual",
|
||||
"Changes your display nickname in the current room only": "Canvia el teu àlies de visualització només en la sala actual",
|
||||
"Double check that your server supports the room version chosen and try again.": "Comprova que el teu servidor és compatible amb la versió de sala que has triat i torna-ho a intentar.",
|
||||
"You do not have the required permissions to use this command.": "No disposes dels permisos necessaris per utilitzar aquesta ordre.",
|
||||
"Sends a message as html, without interpreting it as markdown": "Envia un missatge com a html sense interpretar-lo com a markdown",
|
||||
"Sends a message as plain text, without interpreting it as markdown": "Envia un missatge com a text pla sense interpretar-lo com a markdown",
|
||||
"Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Afegeix ( ͡° ͜ʖ ͡°) al principi d'un missatge de text pla",
|
||||
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Afegeix ¯\\_(ツ)_/¯ al principi d'un missatge de text pla",
|
||||
"Other": "Altres",
|
||||
"Actions": "Accions",
|
||||
"Messages": "Missatges",
|
||||
"Setting up keys": "Configurant claus",
|
||||
"Go Back": "Torna",
|
||||
"Are you sure you want to cancel entering passphrase?": "Estàs segur que vols cancel·lar la introducció de la frase secreta?",
|
||||
"Cancel entering passphrase?": "Vols cancel·lar la introducció de la frase secreta?",
|
||||
"Custom (%(level)s)": "Personalitzat (%(level)s)",
|
||||
"Sign In": "Inicia sessió",
|
||||
"Create Account": "Crea un compte",
|
||||
"Use your account or create a new one to continue.": "Utilitza el teu compte o crea'n un de nou per continuar.",
|
||||
"Sign In or Create Account": "Inicia sessió o Crea un compte",
|
||||
"%(name)s is requesting verification": "%(name)s està demanant verificació",
|
||||
"Trust": "Confiança",
|
||||
"Only continue if you trust the owner of the server.": "Continua, només, si confies amb el propietari del servidor.",
|
||||
"Identity server has no terms of service": "El servidor d'identitat no disposa de condicions de servei",
|
||||
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Aquesta acció necessita accedir al servidor d'identitat predeterminat <server /> per validar una adreça de correu electrònic o un número de telèfon, però el servidor no disposa de condicions de servei.",
|
||||
"Room name or address": "Nom o adreça de la sala",
|
||||
"Name or Matrix ID": "Nom o ID de Matrix",
|
||||
"The server does not support the room version specified.": "El servidor no és compatible amb la versió de sala especificada.",
|
||||
"The file '%(fileName)s' failed to upload.": "No s'ha pogut pujar el fitxer '%(fileName)s'.",
|
||||
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "En aquests moments no és possible respondre amb un fitxer. Vols pujar aquest fitxer sense respondre?",
|
||||
"Replying With Files": "Responent amb fitxers",
|
||||
"Answered Elsewhere": "Respost en una altra banda",
|
||||
"Your user agent": "El teu agent d'usuari",
|
||||
"Find a room… (e.g. %(exampleRoom)s)": "Cerca un sala... (p.e. %(exampleRoom)s)",
|
||||
"e.g. my-room": "p.e. la-meva-sala",
|
||||
"New published address (e.g. #alias:server)": "Nova adreça publicada (p.e. #alias:server)",
|
||||
"Whether you're using %(brand)s as an installed Progressive Web App": "Si estàs utilitzant %(brand)s com a una instal·lació d'una Aplicació Web Progressiva (PWA)",
|
||||
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Si estàs utilitzant, o no, la funció 'molles de pa' (avatars a sobre la llista de sales)",
|
||||
"Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Si estàs utilitzant %(brand)s en un dispositiu on el tàctil és el principal mecanisme d'entrada",
|
||||
"If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si no has eliminat el mètode de recuperació, pot ser que un atacant estigui intentant accedir al teu compte. Canvia la teva contrasenya i configura un nou mètode de recuperació a Configuració, immediatament.",
|
||||
"If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si no has configurat el teu mètode de recuperació, pot ser que un atacant estigui intentant accedir al teu compte. Canvia la teva contrasenya i configura un nou mètode de recuperació a Configuració, immediatament.",
|
||||
"You can also set up Secure Backup & manage your keys in Settings.": "També pots configurar la còpia de seguretat segura i gestionar les teves claus a Configuració.",
|
||||
"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.": "Prèviament has fet servir %(brand)s a %(host)s amb la càrrega mandrosa de membres activada. En aquesta versió la càrrega mandrosa està desactivada. Com que la memòria cau local no és compatible entre les dues versions, %(brand)s necessita tornar a sincronitzar el teu compte.",
|
||||
"Show info about bridges in room settings": "Mostra informació d'enllaços a la configuració de sala",
|
||||
"Use an identity server to invite by email. Manage in Settings.": "Utilitza un servidor d'identitat per convidar per correu electrònic. Gestiona'l a Configuració.",
|
||||
"Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Utilitza un servidor d'identitat per poder convidar per correu electrònic. Fes clic a continua per a utilitzar el servidor d'identitat predeterminat (%(defaultIdentityServerName)s) o gestiona'l a Configuració.",
|
||||
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Utilitza un servidor d'identitat per poder convidar per correu electrònic. <default>Utilitza el predeterminat (%(defaultIdentityServerName)s)</default> o gestiona'l a <settings>Configuració</settings>.",
|
||||
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Utilitza un servidor d'identitat per poder convidar per correu electrònic. Gestiona'l a <settings>Configuració</settings>.",
|
||||
"Integrations not allowed": "No es permeten integracions",
|
||||
"Integrations are disabled": "Les integracions estan desactivades",
|
||||
"Manage integrations": "Gestió d'integracions",
|
||||
"Community settings": "Configuració de comunitat",
|
||||
"Notification settings": "Configuració de notificacions",
|
||||
"Room Settings - %(roomName)s": "Configuració de sala - %(roomName)s",
|
||||
"Confirm this user's session by comparing the following with their User Settings:": "Confirma aquesta sessió d'usuari comparant amb la seva configuració d'usuari, el següent:",
|
||||
"Confirm by comparing the following with the User Settings in your other session:": "Confirma comparant el següent amb la configuració d'usuari de la teva altra sessió:",
|
||||
"Room settings": "Configuració de sala",
|
||||
"Appearance Settings only affect this %(brand)s session.": "La configuració d'aspecte només afecta aquesta sessió %(brand)s.",
|
||||
"Change notification settings": "Canvia la configuració de notificacions",
|
||||
"⚠ These settings are meant for advanced users.": "⚠ Aquesta configuració està pensada per usuaris avançats.",
|
||||
"Link this email with your account in Settings to receive invites directly in %(brand)s.": "Per rebre invitacions directament a %(brand)s, enllaça aquest correu electrònic amb el teu compte a Configuració.",
|
||||
"Change settings": "Canvia la configuració",
|
||||
"Use an identity server in Settings to receive invites directly in %(brand)s.": "Per rebre invitacions directament a %(brand)s, utilitza un servidor d'identitat a Configuració.",
|
||||
"Share this email in Settings to receive invites directly in %(brand)s.": "Per rebre invitacions directament a %(brand)s, comparteix aquest correu electrònic a Configuració.",
|
||||
"Enable 'Manage Integrations' in Settings to do this.": "Per fer això, activa 'Gestió d'integracions' a Configuració.",
|
||||
"We recommend you change your password and recovery key in Settings immediately": "Et recomanem que canviïs immediatament la teva contrasenya i clau de recuperació a Configuració",
|
||||
"Go to Settings": "Ves a Configuració",
|
||||
"No identity server is configured: add one in server settings to reset your password.": "No hi ha cap servidor d'identitat configurat: afegeix-ne un a la configuració del servidor per poder restablir la teva contrasenya.",
|
||||
"User settings": "Configuració d'usuari",
|
||||
"All settings": "Totes les configuracions",
|
||||
"This will end the conference for everyone. Continue?": "Això finalitzarà la conferència per a tothom. Vols continuar?",
|
||||
"End conference": "Finalitza la conferència",
|
||||
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Com a alternativa, pots provar d'utilitzar el servidor públic a <code>turn.matrix.org</code>, però no serà tant fiable i compartirà la teva IP amb el servidor. També pots gestionar-ho a Configuració.",
|
||||
"Try using turn.matrix.org": "Prova d'utilitzar turn.matrix.org",
|
||||
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Demana a l'administrador del servidor local (<code>%(homeserverDomain)s</code>) que configuri un servidor TURN perquè les trucades funcionin de manera fiable.",
|
||||
"Call failed due to misconfigured server": "La trucada ha fallat a causa d'una configuració errònia al servidor",
|
||||
"The call was answered on another device.": "La trucada s'ha respost des d'un altre dispositiu.",
|
||||
"The call could not be established": "No s'ha pogut establir la trucada",
|
||||
"The other party declined the call.": "L'altra part ha rebutjat la trucada.",
|
||||
"Call Declined": "Trucada rebutjada",
|
||||
"Add Phone Number": "Afegeix número de telèfon",
|
||||
"Confirm adding email": "Confirma l'addició del correu electrònic",
|
||||
"To continue, use Single Sign On to prove your identity.": "Per continuar, utilitza la inscripció única SSO (per demostrar la teva identitat).",
|
||||
"Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirma la desactivació del teu compte mitjançant la inscripció única SSO (per demostrar la teva identitat).",
|
||||
"Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Confirma l'eliminació d'aquesta sessió mitjançant la inscripció única SSO (per demostrar la teva identitat).",
|
||||
"Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Confirma l'eliminació d'aquestes sessions mitjançant la inscripció única SSO (per demostrar la teva identitat).",
|
||||
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Confirma l'addició d'aquest número de telèfon mitjançant la inscripció única SSO (per demostrar la teva identitat).",
|
||||
"Confirm adding this email address by using Single Sign On to prove your identity.": "Confirma l'addició d'aquest correu electrònic mitjançant la inscripció única SSO (per demostrar la teva identitat).",
|
||||
"Sign in with single sign-on": "Inicia sessió mitjançant la inscripció única (SSO)",
|
||||
"Single Sign On": "Inscripció única (SSO)",
|
||||
"Use Single Sign On to continue": "Utilitza la inscripció única (SSO) per a continuar",
|
||||
"Click the button below to confirm adding this phone number.": "Fes clic al botó de sota per confirmar l'addició d'aquest número de telèfon.",
|
||||
"Confirm adding phone number": "Confirma l'addició del número de telèfon",
|
||||
"Add Email Address": "Afegeix una adreça de correu electrònic",
|
||||
"Confirm": "Confirma",
|
||||
"Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic."
|
||||
}
|
||||
|
||||
+368
-7
@@ -301,7 +301,7 @@
|
||||
"Drop file here to upload": "Datei hier loslassen zum hochladen",
|
||||
"Idle": "Untätig",
|
||||
"Ongoing conference call%(supportedText)s.": "Laufendes Konferenzgespräch%(supportedText)s.",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Du wirst jetzt auf die Website eines Drittanbieters weitergeleitet, damit du dein Benutzerkonto für die Verwendung von %(integrationsUrl)s authentifizieren kannst. Möchtest du fortfahren?",
|
||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Du wirst jetzt auf die Website eines Drittanbieters weitergeleitet, damit du dein Benutzerkonto für die Verwendung von %(integrationsUrl)s authentifizieren kannst. Möchtest du fortfahren?",
|
||||
"Start automatically after system login": "Nach System-Login automatisch starten",
|
||||
"Jump to first unread message.": "Zur ersten ungelesenen Nachricht springen.",
|
||||
"Options": "Optionen",
|
||||
@@ -711,7 +711,7 @@
|
||||
"Enter keywords separated by a comma:": "Schlüsselwörter kommagetrennt eingeben:",
|
||||
"Forward Message": "Nachricht weiterleiten",
|
||||
"You have successfully set a password and an email address!": "Du hast erfolgreich ein Passwort und eine E-Mail-Adresse gesetzt!",
|
||||
"Remove %(name)s from the directory?": "Soll der Raum %(name)s aus dem Verzeichnis entfernt werden?",
|
||||
"Remove %(name)s from the directory?": "Soll der Raum %(name)s aus dem Verzeichnis entfernt werden?",
|
||||
"%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s nutzt zahlreiche fortgeschrittene Browser-Funktionen, die teilweise in deinem aktuell verwendeten Browser noch nicht verfügbar sind oder sich noch im experimentellen Status befinden.",
|
||||
"Developer Tools": "Entwicklerwerkzeuge",
|
||||
"Preparing to send logs": "Senden von Logs wird vorbereitet",
|
||||
@@ -1099,7 +1099,7 @@
|
||||
"Anchor": "Anker",
|
||||
"Headphones": "Kopfhörer",
|
||||
"Folder": "Ordner",
|
||||
"Pin": "Stecknadel",
|
||||
"Pin": "Anheften",
|
||||
"Timeline": "Chatverlauf",
|
||||
"Autocomplete delay (ms)": "Verzögerung zur Autovervollständigung (ms)",
|
||||
"Roles & Permissions": "Rollen & Berechtigungen",
|
||||
@@ -2056,7 +2056,7 @@
|
||||
"Your new session is now verified. Other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Andere Benutzer sehen sie als vertrauenswürdig an.",
|
||||
"well formed": "wohlgeformt",
|
||||
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Wenn du <server /> nicht verwenden willst, um Kontakte zu finden und von anderen gefunden zu werden, trage unten einen anderen Identitätsserver ein.",
|
||||
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Wenn du einen sicherheitsrelevaten Fehler melden möchtest, lies bitte die Matrix.org <a>Security Disclosure Policy</a>.",
|
||||
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Wenn du einen sicherheitsrelevanten Fehler melden möchtest, lies bitte die Matrix.org <a>Security Disclosure Policy</a>.",
|
||||
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Beim Ändern der Anforderungen für Benutzerrechte ist ein Fehler aufgetreten. Stelle sicher dass du die nötigen Berechtigungen besitzt und versuche es erneut.",
|
||||
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Beim Ändern der Benutzerrechte ist ein Fehler aufgetreten. Stelle sicher dass du die nötigen Berechtigungen besitzt und versuche es erneut.",
|
||||
"Unable to share email address": "E-Mail Adresse konnte nicht geteilt werden",
|
||||
@@ -2187,8 +2187,8 @@
|
||||
"Font size": "Schriftgröße",
|
||||
"IRC display name width": "Breite des IRC Anzeigenamens",
|
||||
"Size must be a number": "Größe muss eine Zahl sein",
|
||||
"Custom font size can only be between %(min)s pt and %(max)s pt": "Eigene Schriftgröße kann nur eine Zahl zwischen %(min)s pt und %(max)s pt sein",
|
||||
"Use between %(min)s pt and %(max)s pt": "Verwende eine Zahl zwischen %(min)s pt und %(max)s pt",
|
||||
"Custom font size can only be between %(min)s pt and %(max)s pt": "Eigene Schriftgröße kann nur eine Zahl zwischen %(min)s pt und %(max)s pt sein",
|
||||
"Use between %(min)s pt and %(max)s pt": "Verwende eine Zahl zwischen %(min)s pt und %(max)s pt",
|
||||
"Appearance": "Erscheinungsbild",
|
||||
"Create room": "Raum erstellen",
|
||||
"Jump to oldest unread message": "Zur ältesten ungelesenen Nachricht springen",
|
||||
@@ -2552,5 +2552,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 d’Ivoire": "Tschechische Republik",
|
||||
"Cyprus": "Zypern",
|
||||
"Curaçao": "Curaçao",
|
||||
"Cuba": "Kuba",
|
||||
"Croatia": "Kroatien",
|
||||
"Cook Islands": "Cookinseln",
|
||||
"Costa Rica": "Costa Rica",
|
||||
"Congo - Kinshasa": "Republik Kongo - Kinshasa",
|
||||
"Congo - Brazzaville": "Republik Kongo - Brazzaville",
|
||||
"Comoros": "Komoren",
|
||||
"Colombia": "Kolumbien",
|
||||
"Cocos (Keeling) Islands": "Kokosinseln (Keeling)",
|
||||
"Christmas Island": "Weihnachtsinsel",
|
||||
"China": "China",
|
||||
"Chile": "Chile",
|
||||
"Chad": "Tschad",
|
||||
"Central African Republic": "Zentralafrikanische Republik",
|
||||
"Cayman Islands": "Kaimaninseln",
|
||||
"Caribbean Netherlands": "Karibische Niederlande",
|
||||
"Cape Verde": "Kap Verde",
|
||||
"Canada": "Kanada",
|
||||
"Cameroon": "Kamerun",
|
||||
"Cambodia": "Kambodscha",
|
||||
"Burundi": "Burundi",
|
||||
"Burkina Faso": "Burkina Faso",
|
||||
"Bulgaria": "Bulgarien",
|
||||
"Brunei": "Brunei",
|
||||
"British Virgin Islands": "Britische Jungferninseln",
|
||||
"British Indian Ocean Territory": "Britisches Territorium im Indischen Ozean",
|
||||
"Brazil": "Brasilien",
|
||||
"Bouvet Island": "Bouvetinsel",
|
||||
"Botswana": "Botswana",
|
||||
"Bosnia": "Bosnien",
|
||||
"Bolivia": "Bolivien",
|
||||
"Bhutan": "Bhutan",
|
||||
"Bermuda": "Bermuda",
|
||||
"Benin": "Benin",
|
||||
"Belize": "Belize",
|
||||
"Belgium": "Belgien",
|
||||
"Belarus": "Weißrussland",
|
||||
"Barbados": "Barbados",
|
||||
"Bangladesh": "Bangladesch",
|
||||
"Bahrain": "Bahrain",
|
||||
"Bahamas": "Bahamas",
|
||||
"Azerbaijan": "Aserbaidschan",
|
||||
"Austria": "Österreich",
|
||||
"Australia": "Australien",
|
||||
"Aruba": "Aruba",
|
||||
"Armenia": "Armenien",
|
||||
"Argentina": "Argentinien",
|
||||
"Antigua & Barbuda": "Antigua und Barbuda",
|
||||
"Antarctica": "Antarktis",
|
||||
"Anguilla": "Anguilla",
|
||||
"Angola": "Angola",
|
||||
"Andorra": "Andorra",
|
||||
"American Samoa": "Amerikanisch-Samoa",
|
||||
"Algeria": "Algerien",
|
||||
"Albania": "Albanien",
|
||||
"Åland Islands": "Äland-Inseln",
|
||||
"Afghanistan": "Afghanistan",
|
||||
"United States": "Vereinigte Staaten",
|
||||
"United Kingdom": "Großbritannien"
|
||||
}
|
||||
|
||||
+129
-68
@@ -46,6 +46,13 @@
|
||||
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.",
|
||||
"Try using turn.matrix.org": "Try using turn.matrix.org",
|
||||
"OK": "OK",
|
||||
"Unable to access microphone": "Unable to access microphone",
|
||||
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.",
|
||||
"Unable to access webcam / microphone": "Unable to access webcam / microphone",
|
||||
"Call failed because no webcam or microphone could not be accessed. Check that:": "Call failed because no webcam or microphone could not be accessed. Check that:",
|
||||
"A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
|
||||
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
|
||||
"No other application is using the webcam": "No other application is using the webcam",
|
||||
"Unable to capture screen": "Unable to capture screen",
|
||||
"Existing Call": "Existing Call",
|
||||
"You are already in a call.": "You are already in a call.",
|
||||
@@ -569,6 +576,62 @@
|
||||
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
|
||||
"%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
|
||||
"%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …",
|
||||
"Remain on your screen when viewing another room, when running": "Remain on your screen when viewing another room, when running",
|
||||
"Remain on your screen while running": "Remain on your screen while running",
|
||||
"Send stickers into this room": "Send stickers into this room",
|
||||
"Send stickers into your active room": "Send stickers into your active room",
|
||||
"Change which room you're viewing": "Change which room you're viewing",
|
||||
"Change the topic of this room": "Change the topic of this room",
|
||||
"See when the topic changes in this room": "See when the topic changes in this room",
|
||||
"Change the topic of your active room": "Change the topic of your active room",
|
||||
"See when the topic changes in your active room": "See when the topic changes in your active room",
|
||||
"Change the name of this room": "Change the name of this room",
|
||||
"See when the name changes in this room": "See when the name changes in this room",
|
||||
"Change the name of your active room": "Change the name of your active room",
|
||||
"See when the name changes in your active room": "See when the name changes in your active room",
|
||||
"Change the avatar of this room": "Change the avatar of this room",
|
||||
"See when the avatar changes in this room": "See when the avatar changes in this room",
|
||||
"Change the avatar of your active room": "Change the avatar of your active room",
|
||||
"See when the avatar changes in your active room": "See when the avatar changes in your active room",
|
||||
"Send stickers to this room as you": "Send stickers to this room as you",
|
||||
"See when a sticker is posted in this room": "See when a sticker is posted in this room",
|
||||
"Send stickers to your active room as you": "Send stickers to your active room as you",
|
||||
"See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room",
|
||||
"with an empty state key": "with an empty state key",
|
||||
"with state key %(stateKey)s": "with state key %(stateKey)s",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "Send <b>%(eventType)s</b> events as you in this room",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "See <b>%(eventType)s</b> events posted to this room",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "Send <b>%(eventType)s</b> events as you in your active room",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "See <b>%(eventType)s</b> events posted to your active room",
|
||||
"The <b>%(capability)s</b> capability": "The <b>%(capability)s</b> capability",
|
||||
"Send messages as you in this room": "Send messages as you in this room",
|
||||
"Send messages as you in your active room": "Send messages as you in your active room",
|
||||
"See messages posted to this room": "See messages posted to this room",
|
||||
"See messages posted to your active room": "See messages posted to your active room",
|
||||
"Send text messages as you in this room": "Send text messages as you in this room",
|
||||
"Send text messages as you in your active room": "Send text messages as you in your active room",
|
||||
"See text messages posted to this room": "See text messages posted to this room",
|
||||
"See text messages posted to your active room": "See text messages posted to your active room",
|
||||
"Send emotes as you in this room": "Send emotes as you in this room",
|
||||
"Send emotes as you in your active room": "Send emotes as you in your active room",
|
||||
"See emotes posted to this room": "See emotes posted to this room",
|
||||
"See emotes posted to your active room": "See emotes posted to your active room",
|
||||
"Send images as you in this room": "Send images as you in this room",
|
||||
"Send images as you in your active room": "Send images as you in your active room",
|
||||
"See images posted to this room": "See images posted to this room",
|
||||
"See images posted to your active room": "See images posted to your active room",
|
||||
"Send videos as you in this room": "Send videos as you in this room",
|
||||
"Send videos as you in your active room": "Send videos as you in your active room",
|
||||
"See videos posted to this room": "See videos posted to this room",
|
||||
"See videos posted to your active room": "See videos posted to your active room",
|
||||
"Send general files as you in this room": "Send general files as you in this room",
|
||||
"Send general files as you in your active room": "Send general files as you in your active room",
|
||||
"See general files posted to this room": "See general files posted to this room",
|
||||
"See general files posted to your active room": "See general files posted to your active room",
|
||||
"Send <b>%(msgtype)s</b> messages as you in this room": "Send <b>%(msgtype)s</b> messages as you in this room",
|
||||
"Send <b>%(msgtype)s</b> messages as you in your active room": "Send <b>%(msgtype)s</b> messages as you in your active room",
|
||||
"See <b>%(msgtype)s</b> messages posted to this room": "See <b>%(msgtype)s</b> messages posted to this room",
|
||||
"See <b>%(msgtype)s</b> messages posted to your active room": "See <b>%(msgtype)s</b> messages posted to your active room",
|
||||
"Cannot reach homeserver": "Cannot reach homeserver",
|
||||
"Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin",
|
||||
"Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured",
|
||||
@@ -648,7 +711,7 @@
|
||||
"Unknown App": "Unknown App",
|
||||
"Help us improve %(brand)s": "Help us improve %(brand)s",
|
||||
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
|
||||
"I want to help": "I want to help",
|
||||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
"Review where you’re logged in": "Review where you’re logged in",
|
||||
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
|
||||
@@ -699,6 +762,7 @@
|
||||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||
"Change notification settings": "Change notification settings",
|
||||
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
|
||||
"New spinner design": "New spinner design",
|
||||
"Message Pinning": "Message Pinning",
|
||||
@@ -730,6 +794,8 @@
|
||||
"Enable big emoji in chat": "Enable big emoji in chat",
|
||||
"Send typing notifications": "Send typing notifications",
|
||||
"Show typing notifications": "Show typing notifications",
|
||||
"Use Command + Enter to send a message": "Use Command + Enter to send a message",
|
||||
"Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
|
||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||
"Mirror local video feed": "Mirror local video feed",
|
||||
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
||||
@@ -778,8 +844,10 @@
|
||||
"When rooms are upgraded": "When rooms are upgraded",
|
||||
"My Ban List": "My Ban List",
|
||||
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
||||
"Active call": "Active call",
|
||||
"Call Paused": "Call Paused",
|
||||
"Video Call": "Video Call",
|
||||
"Voice Call": "Voice Call",
|
||||
"Fill Screen": "Fill Screen",
|
||||
"Return to call": "Return to call",
|
||||
"Unknown caller": "Unknown caller",
|
||||
"Incoming voice call": "Incoming voice call",
|
||||
"Incoming video call": "Incoming video call",
|
||||
@@ -894,9 +962,9 @@
|
||||
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
|
||||
"Export E2E room keys": "Export E2E room keys",
|
||||
"Do you want to set an email address?": "Do you want to set an email address?",
|
||||
"Current password": "Current password",
|
||||
"New Password": "New Password",
|
||||
"Confirm password": "Confirm password",
|
||||
"Passwords don't match": "Passwords don't match",
|
||||
"Current password": "Current password",
|
||||
"Change Password": "Change Password",
|
||||
"Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.",
|
||||
"Cross-signing is ready for use.": "Cross-signing is ready for use.",
|
||||
@@ -935,8 +1003,8 @@
|
||||
"Failed to set display name": "Failed to set display name",
|
||||
"Encryption": "Encryption",
|
||||
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s room.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.",
|
||||
"Manage": "Manage",
|
||||
"Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
|
||||
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
|
||||
@@ -1068,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.",
|
||||
@@ -1390,6 +1460,7 @@
|
||||
"Historical": "Historical",
|
||||
"Custom Tag": "Custom Tag",
|
||||
"Can't see what you’re looking for?": "Can't see what you’re looking for?",
|
||||
"Start a new chat": "Start a new chat",
|
||||
"Explore all public rooms": "Explore all public rooms",
|
||||
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
|
||||
"%(count)s results|other": "%(count)s results",
|
||||
@@ -1615,7 +1686,6 @@
|
||||
"Verify by emoji": "Verify by emoji",
|
||||
"Almost there! Is your other session showing the same shield?": "Almost there! Is your other session showing the same shield?",
|
||||
"Almost there! Is %(displayName)s showing the same shield?": "Almost there! Is %(displayName)s showing the same shield?",
|
||||
"Yes": "Yes",
|
||||
"Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.",
|
||||
"In encrypted rooms, verify all users to ensure it’s secure.": "In encrypted rooms, verify all users to ensure it’s secure.",
|
||||
"You've successfully verified your device!": "You've successfully verified your device!",
|
||||
@@ -1827,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",
|
||||
@@ -1887,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",
|
||||
@@ -1921,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",
|
||||
@@ -2045,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 didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.",
|
||||
"This wasn't me": "This wasn't me",
|
||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||
"Continuing without email": "Continuing without email",
|
||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
|
||||
"Email (optional)": "Email (optional)",
|
||||
"Please fill why you're reporting.": "Please fill why you're reporting.",
|
||||
"Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
|
||||
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
|
||||
@@ -2078,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 it’s a good place for many.": "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.",
|
||||
"Sign into your homeserver": "Sign into your homeserver",
|
||||
"We call the places 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",
|
||||
@@ -2123,9 +2210,13 @@
|
||||
"Upload Error": "Upload Error",
|
||||
"Verify other session": "Verify other session",
|
||||
"Verification Request": "Verification Request",
|
||||
"Approve widget permissions": "Approve widget permissions",
|
||||
"This widget would like to:": "This widget would like to:",
|
||||
"Approve": "Approve",
|
||||
"Decline All": "Decline All",
|
||||
"Remember my selection for this widget": "Remember my selection for this widget",
|
||||
"A widget would like to verify your identity": "A widget would like to verify your identity",
|
||||
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.",
|
||||
"Remember my selection for this widget": "Remember my selection for this widget",
|
||||
"Allow": "Allow",
|
||||
"Deny": "Deny",
|
||||
"Wrong file type": "Wrong file type",
|
||||
@@ -2202,68 +2293,43 @@
|
||||
"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.",
|
||||
"Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",
|
||||
"Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:",
|
||||
"An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s",
|
||||
"Please check your email to continue registration.": "Please check your email to continue registration.",
|
||||
"A confirmation email has been sent to %(emailAddress)s": "A confirmation email has been sent to %(emailAddress)s",
|
||||
"Open the link in the email to continue registration.": "Open the link in the email to continue registration.",
|
||||
"Token incorrect": "Token incorrect",
|
||||
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
|
||||
"Please enter the code it contains:": "Please enter the code it contains:",
|
||||
"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...",
|
||||
"The email field must not be blank.": "The email field must not be blank.",
|
||||
"The username field must not be blank.": "The username field must not be blank.",
|
||||
"The phone number field must not be blank.": "The phone number field must not be blank.",
|
||||
"The password field must not be blank.": "The password field must not be blank.",
|
||||
"Enter username": "Enter username",
|
||||
"Enter email address": "Enter email address",
|
||||
"Enter phone number": "Enter 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)",
|
||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||
"Passwords don't match": "Passwords don't match",
|
||||
"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)",
|
||||
"Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
|
||||
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
|
||||
"Enter username": "Enter username",
|
||||
"Email (optional)": "Email (optional)",
|
||||
"Phone (optional)": "Phone (optional)",
|
||||
"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 />",
|
||||
"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",
|
||||
@@ -2272,7 +2338,7 @@
|
||||
"Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.",
|
||||
"Communities": "Communities",
|
||||
"Create community": "Create community",
|
||||
"<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 use 'img' tags\n</p>\n": "<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 use 'img' tags\n</p>\n",
|
||||
"<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 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",
|
||||
"Add rooms to the community summary": "Add rooms to the community summary",
|
||||
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
|
||||
"Add to summary": "Add to summary",
|
||||
@@ -2371,8 +2437,9 @@
|
||||
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
||||
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
||||
"Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s",
|
||||
"Filter": "Filter",
|
||||
"Clear filter": "Clear filter",
|
||||
"Search rooms": "Search rooms",
|
||||
"Filter rooms and people": "Filter rooms and people",
|
||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||
@@ -2380,10 +2447,6 @@
|
||||
"%(count)s of your messages have not been sent.|one": "Your message was not sent.",
|
||||
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
|
||||
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
|
||||
"Calling...": "Calling...",
|
||||
"Call connecting...": "Call connecting...",
|
||||
"Starting camera...": "Starting camera...",
|
||||
"Starting microphone...": "Starting microphone...",
|
||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||
@@ -2395,11 +2458,6 @@
|
||||
"Failed to reject invite": "Failed to reject invite",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
|
||||
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
|
||||
"Fill screen": "Fill screen",
|
||||
"Click to unmute video": "Click to unmute video",
|
||||
"Click to mute video": "Click to mute video",
|
||||
"Click to unmute audio": "Click to unmute audio",
|
||||
"Click to mute audio": "Click to mute audio",
|
||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||
"Failed to load timeline position": "Failed to load timeline position",
|
||||
@@ -2407,6 +2465,8 @@
|
||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
|
||||
"Got an account? <a>Sign in</a>": "Got an account? <a>Sign in</a>",
|
||||
"New here? <a>Create an account</a>": "New here? <a>Create an account</a>",
|
||||
"Notification settings": "Notification settings",
|
||||
"Security & privacy": "Security & privacy",
|
||||
"All settings": "All settings",
|
||||
@@ -2425,12 +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.",
|
||||
@@ -2451,27 +2509,29 @@
|
||||
"Incorrect username and/or password.": "Incorrect username and/or password.",
|
||||
"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",
|
||||
"The phone number entered looks invalid": "The phone number entered looks invalid",
|
||||
"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",
|
||||
"Failed to fetch avatar URL": "Failed to fetch avatar URL",
|
||||
"Set a display name:": "Set a display name:",
|
||||
"Upload an avatar:": "Upload an avatar:",
|
||||
"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 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.",
|
||||
@@ -2632,6 +2692,7 @@
|
||||
"Activate selected button": "Activate selected button",
|
||||
"Toggle right panel": "Toggle right panel",
|
||||
"Toggle this dialog": "Toggle this dialog",
|
||||
"Go to Home View": "Go to Home View",
|
||||
"Move autocomplete selection up/down": "Move autocomplete selection up/down",
|
||||
"Cancel autocomplete": "Cancel autocomplete",
|
||||
"Page Up": "Page Up",
|
||||
|
||||
+160
-1
@@ -2538,5 +2538,164 @@
|
||||
"Add comment": "Aldoni komenton",
|
||||
"Please go into as much detail as you like, so we can track down the problem.": "Bonvolu detaligi tiel multe, kiel vi volas, por ke ni povu pli facile trovi la problemon.",
|
||||
"Tell us below how you feel about %(brand)s so far.": "Diru al ni, kion vi ĝis nun pensas pri %(brand)s.",
|
||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Pratipoj de la 2-a versio de komunumoj. Bezonas konforman hejmservilon. Tre eksperimenta – uzu nur zorge."
|
||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Pratipoj de la 2-a versio de komunumoj. Bezonas konforman hejmservilon. Tre eksperimenta – uzu nur zorge.",
|
||||
"Uzbekistan": "Uzbekujo",
|
||||
"United Arab Emirates": "Unuiĝinaj Arabaj Emirlandoj",
|
||||
"Ukraine": "Ukrainujo",
|
||||
"Uganda": "Ugando",
|
||||
"Turkey": "Turkujo",
|
||||
"Tunisia": "Tunizio",
|
||||
"Tajikistan": "Taĝikujo",
|
||||
"Malta": "Malto",
|
||||
"Mali": "Malio",
|
||||
"Maldives": "Maldivoj",
|
||||
"Malaysia": "Malajzio",
|
||||
"Luxembourg": "Luksemburgo",
|
||||
"Lithuania": "Litovujo",
|
||||
"Liechtenstein": "Liĥtenŝtejno",
|
||||
"Latvia": "Latvujo",
|
||||
"Laos": "Laoso",
|
||||
"Kyrgyzstan": "Kirgizujo",
|
||||
"Kosovo": "Kosovo",
|
||||
"Kenya": "Kenjo",
|
||||
"Kazakhstan": "Kazaĥujo",
|
||||
"Japan": "Japanujo",
|
||||
"Jamaica": "Jamajko",
|
||||
"Italy": "Italujo",
|
||||
"Israel": "Israelo",
|
||||
"Isle of Man": "Manksujo",
|
||||
"Ireland": "Irlando",
|
||||
"Iraq": "Irako",
|
||||
"Iran": "Irano",
|
||||
"Indonesia": "Indonezio",
|
||||
"India": "Barato",
|
||||
"Iceland": "Islando",
|
||||
"Hungary": "Hungarujo",
|
||||
"Hong Kong": "Honkongo",
|
||||
"Honduras": "Honduro",
|
||||
"Haiti": "Haitio",
|
||||
"Guatemala": "Gvatemalo",
|
||||
"Grenada": "Grenado",
|
||||
"Greenland": "Gronlando",
|
||||
"Greece": "Grekujo",
|
||||
"Gibraltar": "Ĝibraltaro",
|
||||
"Germany": "Germanujo",
|
||||
"France": "Francujo",
|
||||
"Finland": "Finlando",
|
||||
"Fiji": "Fiĝio",
|
||||
"Ethiopia": "Etiopujo",
|
||||
"Estonia": "Estonujo",
|
||||
"Eritrea": "Eritreo",
|
||||
"Equatorial Guinea": "Ekvatora Gvineo",
|
||||
"El Salvador": "Salvadoro",
|
||||
"Egypt": "Egiptujo",
|
||||
"Ecuador": "Ekvadoro",
|
||||
"Dominican Republic": "Dominika Respubliko",
|
||||
"Dominica": "Dominiko",
|
||||
"Denmark": "Danujo",
|
||||
"Côte d’Ivoire": "Ebur-Bordo",
|
||||
"Czech Republic": "Ĉeĥujo",
|
||||
"Cyprus": "Kipro",
|
||||
"Cuba": "Kubo",
|
||||
"Croatia": "Kroatujo",
|
||||
"Costa Rica": "Kostariko",
|
||||
"Cook Islands": "Insuloj de Cook",
|
||||
"Comoros": "Komoroj",
|
||||
"Colombia": "Kolombio",
|
||||
"Christmas Island": "Kristnaskinsulo",
|
||||
"China": "Ĉinujo",
|
||||
"Chile": "Ĉilio",
|
||||
"Chad": "Ĉado",
|
||||
"Central African Republic": "Centr-Afriko",
|
||||
"Cape Verde": "Kaboverdo",
|
||||
"Canada": "Kanado",
|
||||
"Cameroon": "Kameruno",
|
||||
"Cambodia": "Kamboĝo",
|
||||
"Burundi": "Burundo",
|
||||
"Bulgaria": "Bulgarujo",
|
||||
"Brunei": "Brunejo",
|
||||
"British Virgin Islands": "Britaj Virgulininsuloj",
|
||||
"Brazil": "Brazilo",
|
||||
"Botswana": "Cvanujo",
|
||||
"Bosnia": "Bosnujo",
|
||||
"Bolivia": "Bolivio",
|
||||
"Bhutan": "Butano",
|
||||
"Bermuda": "Bermudo",
|
||||
"Benin": "Benino",
|
||||
"Belize": "Belizo",
|
||||
"Belgium": "Belgujo",
|
||||
"Belarus": "Belorusujo",
|
||||
"Barbados": "Barbado",
|
||||
"Bangladesh": "Bangladeŝo",
|
||||
"Bahrain": "Barejno",
|
||||
"Bahamas": "Bahamoj",
|
||||
"Azerbaijan": "Azerbajĝano",
|
||||
"Austria": "Aŭstrujo",
|
||||
"Australia": "Aŭstralio",
|
||||
"Aruba": "Arubo",
|
||||
"Armenia": "Armenujo",
|
||||
"Argentina": "Argentino",
|
||||
"Antigua & Barbuda": "Antigvo kaj Barbudo",
|
||||
"Antarctica": "Antarkto",
|
||||
"Angola": "Angolo",
|
||||
"Andorra": "Andoro",
|
||||
"American Samoa": "Usona Samoo",
|
||||
"Algeria": "Alĝerio",
|
||||
"Albania": "Albanujo",
|
||||
"Åland Islands": "Alando",
|
||||
"Afghanistan": "Afganujo",
|
||||
"United States": "Usono",
|
||||
"United Kingdom": "Britujo",
|
||||
"Sri Lanka": "Srilanko",
|
||||
"Spain": "Hispanujo",
|
||||
"South Korea": "Sud-Koreujo",
|
||||
"South Africa": "Sud-Afriko",
|
||||
"Somalia": "Somalujo",
|
||||
"Solomon Islands": "Salomonoj",
|
||||
"Slovenia": "Slovenujo",
|
||||
"Slovakia": "Slovakujo",
|
||||
"Singapore": "Singapuro",
|
||||
"Sierra Leone": "Sieraleono",
|
||||
"Seychelles": "Sejŝeloj",
|
||||
"Serbia": "Serbujo",
|
||||
"Senegal": "Senegalo",
|
||||
"Saudi Arabia": "Sauda Arabujo",
|
||||
"San Marino": "Sanmarino",
|
||||
"Samoa": "Samoo",
|
||||
"Rwanda": "Ruando",
|
||||
"Russia": "Rusujo",
|
||||
"Romania": "Rumanujo",
|
||||
"Qatar": "Kataro",
|
||||
"Puerto Rico": "Portoriko",
|
||||
"Portugal": "Portugalujo",
|
||||
"Poland": "Polujo",
|
||||
"Philippines": "Filipinoj",
|
||||
"Peru": "Peruo",
|
||||
"Paraguay": "Paragvajo",
|
||||
"Papua New Guinea": "Papuo-Nov-Gvineo",
|
||||
"Panama": "Panamo",
|
||||
"Palestine": "Palestino",
|
||||
"Palau": "Palaŭo",
|
||||
"Pakistan": "Pakistano",
|
||||
"Oman": "Omano",
|
||||
"Norway": "Norvegujo",
|
||||
"North Korea": "Nord-Koreujo",
|
||||
"Niue": "Niuo",
|
||||
"Nigeria": "Niĝerio",
|
||||
"Niger": "Niĝero",
|
||||
"New Zealand": "Nov-Zelando",
|
||||
"New Caledonia": "Nova Kaledonio",
|
||||
"Netherlands": "Nederlando",
|
||||
"Nepal": "Nepalo",
|
||||
"Nauru": "Nauro",
|
||||
"Namibia": "Namibio",
|
||||
"Myanmar": "Birmo",
|
||||
"Mozambique": "Mozambiko",
|
||||
"Morocco": "Maroko",
|
||||
"Montserrat": "Moncerato",
|
||||
"Montenegro": "Montenegro",
|
||||
"Caribbean Netherlands": "Kariba Nederlando",
|
||||
"Burkina Faso": "Burkino",
|
||||
"Bouvet Island": "Buvet-Insulo",
|
||||
"Anguilla": "Angvilo"
|
||||
}
|
||||
|
||||
+367
-1
@@ -2562,5 +2562,371 @@
|
||||
"Rate %(brand)s": "Hinda %(brand)s rakendust",
|
||||
"Feedback sent": "Tagasiside on saadetud",
|
||||
"%(senderName)s ended the call": "%(senderName)s lõpetas kõne",
|
||||
"You ended the call": "Sina lõpetasid kõne"
|
||||
"You ended the call": "Sina lõpetasid kõne",
|
||||
"Now, lets help you get started": "Nüüd näitame sulle, mida saad järgmiseks teha",
|
||||
"Welcome %(name)s": "Tere tulemast, %(name)s",
|
||||
"Add a photo so people know it's you.": "Enda tutvustamiseks lisa foto.",
|
||||
"Great, that'll help people know it's you": "Suurepärane, nüüd teised teavad et tegemist on sinuga",
|
||||
"Use the + to make a new room or explore existing ones below": "Uue jututoa tegemiseks või olemasolevatega tutvumiseks klõpsi + märki",
|
||||
"New version of %(brand)s is available": "%(brand)s ralenduse uus versioon on saadaval",
|
||||
"Update %(brand)s": "Uuenda %(brand)s rakendust",
|
||||
"Enable desktop notifications": "Võta kasutusele töölauakeskkonna teavitused",
|
||||
"Don't miss a reply": "Ära jäta vastust vahele",
|
||||
"Now, let's help you get started": "Nüüd näitame sulle, mida saad järgmiseks teha",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Kutsu teist osapoolt tema nime, e-posti aadressi, kasutajanime (nagu <userId/>) alusel või <a>jaga seda jututuba</a>.",
|
||||
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Alusta vestlust kasutades teise osapoole nime, e-posti aadressi või kasutajanime (näiteks <userId/>).",
|
||||
"Invite by email": "Saada kutse e-kirjaga",
|
||||
"Zambia": "Sambia",
|
||||
"Yemen": "Jeemen",
|
||||
"Western Sahara": "Lääne-Sahara",
|
||||
"Wallis & Futuna": "Wallis ja Futuna",
|
||||
"Vietnam": "Vietnam",
|
||||
"Venezuela": "Venezuela",
|
||||
"Vatican City": "Vatikan",
|
||||
"Vanuatu": "Vanuatu",
|
||||
"Uzbekistan": "Usbekistan",
|
||||
"Uruguay": "Uruguay",
|
||||
"United Arab Emirates": "Araabia Ühendemiraadid",
|
||||
"Ukraine": "Ukraina",
|
||||
"Uganda": "Uganda",
|
||||
"U.S. Virgin Islands": "USA Neitsisaared",
|
||||
"Tuvalu": "Tuvalu",
|
||||
"Turks & Caicos Islands": "Turks ja Caicos",
|
||||
"Turkmenistan": "Türkmenistan",
|
||||
"Turkey": "Türgi",
|
||||
"Tunisia": "Tuneesia",
|
||||
"Trinidad & Tobago": "Trinidad ja Tobago",
|
||||
"Tonga": "Tonga",
|
||||
"Tokelau": "Tokelau",
|
||||
"Togo": "Togo",
|
||||
"Timor-Leste": "Ida-Timor",
|
||||
"Thailand": "Tai",
|
||||
"Tanzania": "Tansaania",
|
||||
"Tajikistan": "Tadžikistan",
|
||||
"Taiwan": "Taiwan",
|
||||
"São Tomé & Príncipe": "São Tomé ja Príncipe",
|
||||
"Syria": "Süüria",
|
||||
"Switzerland": "Šveits",
|
||||
"Sweden": "Rootsi",
|
||||
"Swaziland": "Svaasimaa",
|
||||
"Svalbard & Jan Mayen": "Svalbard ja Jan Mayen",
|
||||
"Suriname": "Suriname",
|
||||
"Sudan": "Sudaan",
|
||||
"St. Vincent & Grenadines": "Saint Vincent",
|
||||
"St. Pierre & Miquelon": "Saint-Pierre ja Miquelon",
|
||||
"St. Martin": "Saint-Martin",
|
||||
"St. Lucia": "Saint Lucia",
|
||||
"St. Kitts & Nevis": "Saint Kitts ja Nevis",
|
||||
"St. Helena": "Saint Helena",
|
||||
"St. Barthélemy": "Saint-Barthélemy",
|
||||
"Sri Lanka": "Sri Lanka",
|
||||
"Spain": "Hispaania",
|
||||
"South Sudan": "Lõuna-Sudaan",
|
||||
"South Korea": "Lõuna-Korea",
|
||||
"South Georgia & South Sandwich Islands": "Lõuna-Georgia ja Lõuna-Sandwichi saared",
|
||||
"South Africa": "Lõuna-Aafrika Vabariik",
|
||||
"Somalia": "Somaalia",
|
||||
"Solomon Islands": "Saalomoni Saared",
|
||||
"Slovenia": "Sloveenia",
|
||||
"Slovakia": "Slovakkia",
|
||||
"Sint Maarten": "Sint Maarten",
|
||||
"Singapore": "Singapur",
|
||||
"Sierra Leone": "Sierra Leone",
|
||||
"Seychelles": "Seišellid",
|
||||
"Serbia": "Serbia",
|
||||
"Senegal": "Senegal",
|
||||
"Saudi Arabia": "Saudi Araabia",
|
||||
"San Marino": "San Marino",
|
||||
"Samoa": "Samoa",
|
||||
"Réunion": "Réunion",
|
||||
"Rwanda": "Rwanda",
|
||||
"Russia": "Venemaa",
|
||||
"Romania": "Rumeenia",
|
||||
"Qatar": "Katar",
|
||||
"Puerto Rico": "Puerto Rico",
|
||||
"Portugal": "Portugal",
|
||||
"Poland": "Poola",
|
||||
"Pitcairn Islands": "Pitcairn",
|
||||
"Philippines": "Filipiinid",
|
||||
"Peru": "Peruu",
|
||||
"Paraguay": "Paraguay",
|
||||
"Papua New Guinea": "Paapua Uus-Guinea",
|
||||
"Panama": "Panama",
|
||||
"Palestine": "Palestiina",
|
||||
"Palau": "Belau",
|
||||
"Pakistan": "Pakistan",
|
||||
"Oman": "Omaan",
|
||||
"Norway": "Norra",
|
||||
"Northern Mariana Islands": "Põhja-Mariaanid",
|
||||
"North Korea": "Põhja-Korea",
|
||||
"Norfolk Island": "Norfolk",
|
||||
"Niue": "Niue",
|
||||
"Nigeria": "Nigeeria",
|
||||
"Niger": "Niger",
|
||||
"Nicaragua": "Nicaragua",
|
||||
"New Zealand": "Uus-Meremaa",
|
||||
"New Caledonia": "Uus-Kaledoonia",
|
||||
"Netherlands": "Holland",
|
||||
"Nepal": "Nepal",
|
||||
"Nauru": "Nauru",
|
||||
"Namibia": "Namiibia",
|
||||
"Myanmar": "Myanmar",
|
||||
"Mozambique": "Mosambiik",
|
||||
"Morocco": "Maroko",
|
||||
"Montserrat": "Montserrat",
|
||||
"Montenegro": "Montenegro",
|
||||
"Mongolia": "Mongoolia",
|
||||
"Monaco": "Monaco",
|
||||
"Moldova": "Moldova",
|
||||
"Micronesia": "Mikroneesia",
|
||||
"Mexico": "Mehhiko",
|
||||
"Mayotte": "Mayotte",
|
||||
"Mauritius": "Mauritius",
|
||||
"Mauritania": "Mauritaania",
|
||||
"Martinique": "Martinique",
|
||||
"Marshall Islands": "Marshalli Saared",
|
||||
"Malta": "Malta",
|
||||
"Mali": "Mali",
|
||||
"Maldives": "Maldiivid",
|
||||
"Malaysia": "Malaisia",
|
||||
"Malawi": "Malawi",
|
||||
"Madagascar": "Madagaskar",
|
||||
"Macedonia": "Põhja-Makedoonia",
|
||||
"Macau": "Macau",
|
||||
"Luxembourg": "Luksemburg",
|
||||
"Lithuania": "Leedu",
|
||||
"Liechtenstein": "Liechtenstein",
|
||||
"Libya": "Liibüa",
|
||||
"Liberia": "Libeeria",
|
||||
"Lesotho": "Lesotho",
|
||||
"Lebanon": "Liibanon",
|
||||
"Latvia": "Läti",
|
||||
"Laos": "Laos",
|
||||
"Kyrgyzstan": "Kõrgõzstan",
|
||||
"Kuwait": "Kuveit",
|
||||
"Kosovo": "Kosovo",
|
||||
"Kiribati": "Kiribati",
|
||||
"Kenya": "Keenia",
|
||||
"Kazakhstan": "Kasahstan",
|
||||
"Jordan": "Jordaania",
|
||||
"Jersey": "Jersey",
|
||||
"Japan": "Jaapan",
|
||||
"Jamaica": "Jamaica",
|
||||
"Italy": "Itaalia",
|
||||
"Israel": "Iisrael",
|
||||
"Isle of Man": "Mani saar",
|
||||
"Ireland": "Iirimaa",
|
||||
"Iraq": "Iraak",
|
||||
"Iran": "Iraan",
|
||||
"Indonesia": "Indoneesia",
|
||||
"India": "India",
|
||||
"Iceland": "Island",
|
||||
"Hungary": "Ungari",
|
||||
"Hong Kong": "Hongkong",
|
||||
"Honduras": "Honduras",
|
||||
"Heard & McDonald Islands": "Heard ja McDonald",
|
||||
"Haiti": "Haiti",
|
||||
"Guyana": "Guyana",
|
||||
"Guinea-Bissau": "Guinea-Bissau",
|
||||
"Guinea": "Guinea",
|
||||
"Guernsey": "Guernsey",
|
||||
"Guatemala": "Guatemala",
|
||||
"Guam": "Guam",
|
||||
"Guadeloupe": "Guadeloupe",
|
||||
"Grenada": "Grenada",
|
||||
"Greenland": "Gröönimaa",
|
||||
"Greece": "Kreeka",
|
||||
"Gibraltar": "Gibraltar",
|
||||
"Ghana": "Ghana",
|
||||
"Germany": "Saksamaa",
|
||||
"Georgia": "Gruusia",
|
||||
"Gambia": "Gambia",
|
||||
"Gabon": "Gabon",
|
||||
"French Southern Territories": "Prantsuse Lõunaalad",
|
||||
"French Polynesia": "Prantsuse Polüneesia",
|
||||
"French Guiana": "Prantsuse Guajaana",
|
||||
"France": "Prantsusmaa",
|
||||
"Zimbabwe": "Zimbabwe",
|
||||
"Finland": "Soome",
|
||||
"Fiji": "Fidži",
|
||||
"Faroe Islands": "Fääri saared",
|
||||
"Falkland Islands": "Falklandi (Malviini) saared",
|
||||
"Ethiopia": "Etioopia",
|
||||
"Estonia": "Eesti",
|
||||
"Eritrea": "Eritrea",
|
||||
"Equatorial Guinea": "Ekvatoriaal-Guinea",
|
||||
"El Salvador": "El Salvador",
|
||||
"Egypt": "Egiptus",
|
||||
"Ecuador": "Ecuador",
|
||||
"Dominican Republic": "Dominikaani Vabariik",
|
||||
"Dominica": "Dominica",
|
||||
"Djibouti": "Djibouti",
|
||||
"Denmark": "Taani",
|
||||
"Côte d’Ivoire": "Elevandiluurannik",
|
||||
"Czech Republic": "Tšehhi",
|
||||
"Cyprus": "Küpros",
|
||||
"Curaçao": "Curaçao",
|
||||
"Cuba": "Kuuba",
|
||||
"Croatia": "Horvaatia",
|
||||
"Costa Rica": "Costa Rica",
|
||||
"Cook Islands": "Cooki saared",
|
||||
"Congo - Kinshasa": "Kongo DV",
|
||||
"Congo - Brazzaville": "Kongo Vabariik",
|
||||
"Comoros": "Komoorid",
|
||||
"Colombia": "Colombia",
|
||||
"Cocos (Keeling) Islands": "Kookossaared",
|
||||
"Christmas Island": "Jõulusaar",
|
||||
"China": "Hiina",
|
||||
"Chile": "Tšiili",
|
||||
"Chad": "Tšaad",
|
||||
"Central African Republic": "Kesk-Aafrika Vabariik",
|
||||
"Cayman Islands": "Kaimanisaared",
|
||||
"Caribbean Netherlands": "Bonaire, Sint Eustatius ja Saba",
|
||||
"Cape Verde": "Roheneemesaared",
|
||||
"Canada": "Kanada",
|
||||
"Cameroon": "Kamerun",
|
||||
"Cambodia": "Kambodža",
|
||||
"Burundi": "Burundi",
|
||||
"Burkina Faso": "Burkina Faso",
|
||||
"Bulgaria": "Bulgaaria",
|
||||
"Brunei": "Brunei",
|
||||
"British Virgin Islands": "Briti Neitsisaared",
|
||||
"British Indian Ocean Territory": "Briti India ookeani ala",
|
||||
"Brazil": "Brasiilia",
|
||||
"Bouvet Island": "Bouvet’ saar",
|
||||
"Botswana": "Botswana",
|
||||
"Bosnia": "Bosnia ja Hertsegoviina",
|
||||
"Bolivia": "Boliivia",
|
||||
"Bhutan": "Bhutan",
|
||||
"Bermuda": "Bermuda",
|
||||
"Benin": "Benin",
|
||||
"Belize": "Belize",
|
||||
"Belgium": "Belgia",
|
||||
"Belarus": "Valgevene",
|
||||
"Barbados": "Barbados",
|
||||
"Bangladesh": "Bangladesh",
|
||||
"Bahrain": "Bahrein",
|
||||
"Bahamas": "Bahama",
|
||||
"Azerbaijan": "Aserbaidžaan",
|
||||
"Austria": "Austria",
|
||||
"Australia": "Austraalia",
|
||||
"Aruba": "Aruba",
|
||||
"Armenia": "Armeenia",
|
||||
"Argentina": "Argentina",
|
||||
"Antigua & Barbuda": "Antigua ja Barbuda",
|
||||
"Antarctica": "Antarktis",
|
||||
"Anguilla": "Anguilla",
|
||||
"Angola": "Angola",
|
||||
"Andorra": "Andorra",
|
||||
"American Samoa": "Ameerika Samoa",
|
||||
"Algeria": "Alžeeria",
|
||||
"Albania": "Albaania",
|
||||
"Åland Islands": "Ahvenamaa",
|
||||
"Afghanistan": "Afganistan",
|
||||
"United States": "Ameerika Ühendriigid",
|
||||
"United Kingdom": "Suurbritannia",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Sõnumid siin jututoas on läbivalt krüptitud. Kui uued kasutajad liituvad, siis klõpsides nende tunnuspilti saad kontrollida nende profiili.",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Sõnumid siin jututoas on läbivalt krüptitud. Klõpsides tunnuspilti saad kontrollida kasutaja %(displayName)s profiili.",
|
||||
"%(creator)s created this DM.": "%(creator)s alustas seda otsesuhtlust.",
|
||||
"This is the start of <roomName/>.": "See on <roomName/> jututoa algus.",
|
||||
"Add a photo, so people can easily spot your room.": "Selle, et teised märkaks sinu jututuba lihtsamini, palun lisa üks pilt.",
|
||||
"%(displayName)s created this room.": "%(displayName)s lõi selle jututoa.",
|
||||
"You created this room.": "Sa lõid selle jututoa.",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "Selleks, et teised teaks millega on tegemist, palun <a>lisa teema</a>.",
|
||||
"Topic: %(topic)s ": "Teema: %(topic)s ",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Teema: %(topic)s (<a>muudetud</a>)",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "See on sinu ja kasutaja <displayName/> otsesuhtluse ajaloo algus.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Seni kuni emb-kumb teist kolmandaid osapooli liituma ei kutsu, olete siin vestluses vaid teie kahekesi.",
|
||||
"Call Paused": "Kõne on ajutiselt peatatud",
|
||||
"Takes the call in the current room off hold": "Võtab selles jututoas ootel oleva kõne",
|
||||
"Places the call in the current room on hold": "Jätab kõne selles jututoas ootele",
|
||||
"Role": "Roll",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(count)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 %(count)s rooms.|other": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(count)s jututoa andmete salvestamiseks kulub hetkel %(size)s.",
|
||||
"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",
|
||||
"<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"
|
||||
}
|
||||
|
||||
@@ -151,5 +151,12 @@
|
||||
"A new version of %(brand)s is available!": "نگارشی جدید از %(brand)s موجود است!",
|
||||
"Guest": "مهمان",
|
||||
"Confirm adding this email address by using Single Sign On to prove your identity.": "برای تأیید هویتتان، این نشانی رایانامه را با ورود یکپارچه تأیید کنید.",
|
||||
"Click the button below to confirm adding this email address.": "برای تأیید افزودن این نشانی رایانامه، دکمهٔ زیر را بزنید."
|
||||
"Click the button below to confirm adding this email address.": "برای تأیید افزودن این نشانی رایانامه، دکمهٔ زیر را بزنید.",
|
||||
"Your homeserver's URL": "آدرس سرور شما",
|
||||
"Whether or not you're using the Richtext mode of the Rich Text Editor": "از حالت Richtext ویرایشگر متنی استفاده می کنید یا نه",
|
||||
"Which officially provided instance you are using, if any": "در صورت وجود ، از کدام سرویس رسمی استفاده میکنید",
|
||||
"Whether or not you're logged in (we don't record your username)": "وارد حساب خود میشوید یا خیر (ما نام کاربری شما را ثبت نمیکنیم)",
|
||||
"Click the button below to confirm adding this phone number.": "برای تائید اضافهشدن این شماره تلفن، بر روی دکمهی زیر کلیک کنید.",
|
||||
"Confirm adding this phone number by using Single Sign On to prove your identity.": "برای اثبات هویت خود، اضافهشدن این شماره تلفن را با استفاده از Single Sign On تائید کنید.",
|
||||
"Failed to verify email address: make sure you clicked the link in the email": "خطا در تائید آدرس ایمیل: مطمئن شوید که بر روی لینک موجود در ایمیل کلیک کرده اید"
|
||||
}
|
||||
|
||||
+116
-3
@@ -1631,7 +1631,7 @@
|
||||
"Discovery options will appear once you have added a phone number above.": "Etsinnän asetukset näkyvät sen jälkeen, kun olet lisännyt puhelinnumeron.",
|
||||
"Failed to connect to integration manager": "Yhdistäminen integraatioiden lähteeseen epäonnistui",
|
||||
"Trusted": "Luotettu",
|
||||
"Not trusted": "Epäluotettu",
|
||||
"Not trusted": "Ei-luotettu",
|
||||
"This client does not support end-to-end encryption.": "Tämä asiakasohjelma ei tue osapuolten välistä salausta.",
|
||||
"Messages in this room are not end-to-end encrypted.": "Tämän huoneen viestit eivät ole salattuja.",
|
||||
"Messages in this room are end-to-end encrypted.": "Tämän huoneen viestit ovat salattuja.",
|
||||
@@ -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 you’re looking for?": "Etkö löydä, mitä etsit?",
|
||||
"This is the start of <roomName/>.": "Tästä alkaa <roomName/>.",
|
||||
"%(displayName)s created this room.": "%(displayName)s loi tämän huoneen.",
|
||||
"You created this room.": "Loit tämän huoneen.",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Lisää aihe</a>, jotta ihmiset tietävät mistä on kyse.",
|
||||
"Topic: %(topic)s ": "Aihe: %(topic)s ",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Aihe: %(topic)s (<a>muokkaa</a>)",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "Tästä alkaa yksityisviestihistoriasi käyttäjän <displayName/> kanssa.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Vain te kaksi olette tässä keskustelussa, ellei jompi kumpi kutsu muita.",
|
||||
"Remove messages sent by others": "Poista toisten lähettämät viestit",
|
||||
"Privacy": "Tietosuoja",
|
||||
"not ready": "ei valmis",
|
||||
"ready": "valmis",
|
||||
"unexpected type": "odottamaton tyyppi",
|
||||
"Algorithm:": "Algoritmi:",
|
||||
"Failed to save your profile": "Profiilisi tallentaminen ei onnistunut",
|
||||
"Your server isn't responding to some <a>requests</a>.": "Palvelimesi ei vastaa joihinkin <a>pyyntöihin</a>.",
|
||||
"Incoming call": "Saapuva puhelu",
|
||||
"Incoming video call": "Saapuva videopuhelu",
|
||||
"Incoming voice call": "Saapuva äänipuhelu",
|
||||
"Unknown caller": "Tuntematon soittaja",
|
||||
"Enable experimental, compact IRC style layout": "Ota käyttöön kokeellinen, IRC-tyylinen asettelu",
|
||||
"Use Ctrl + Enter to send a message": "Ctrl + Enter lähettää viestin",
|
||||
"Use Command + Enter to send a message": "Komento + Enter lähettää viestin",
|
||||
"%(senderName)s ended the call": "%(senderName)s lopetti puhelun",
|
||||
"You ended the call": "Lopetit puhelun",
|
||||
"New version of %(brand)s is available": "%(brand)s-sovelluksesta on saatavilla uusi versio",
|
||||
"Update %(brand)s": "Päivitä %(brand)s",
|
||||
"Enable desktop notifications": "Ota työpöytäilmoitukset käyttöön",
|
||||
"Takes the call in the current room off hold": "Ottaa nykyisen huoneen puhelun pois pidosta",
|
||||
"Places the call in the current room on hold": "Asettaa nykyisen huoneen puhelun pitoon",
|
||||
"Away": "Poissa",
|
||||
"A confirmation email has been sent to %(emailAddress)s": "Vahvistussähköposti on lähetetty osoitteeseen %(emailAddress)s",
|
||||
"Open the link in the email to continue registration.": "Jatka rekisteröitymistä avaamalla sähköpostissa oleva linkki.",
|
||||
"Enter email address": "Syötä sähköpostiosoite",
|
||||
"Enter phone number": "Syötä puhelinnumero",
|
||||
"Now, let's help you get started": "Autetaanpa sinut alkuun",
|
||||
"delete the address.": "poista osoite.",
|
||||
"Filter rooms and people": "Suodata huoneita ja ihmisiä",
|
||||
"Go to Home View": "Siirry kotinäkymään",
|
||||
"Community and user menu": "Yhteisö- ja käyttäjävalikko",
|
||||
"Decline All": "Kieltäydy kaikista",
|
||||
"Approve": "Hyväksy",
|
||||
"Your area is experiencing difficulties connecting to the internet.": "Alueellasi on ongelmia internet-yhteyksissä.",
|
||||
"The server is offline.": "Palvelin ei ole verkossa.",
|
||||
"Your firewall or anti-virus is blocking the request.": "Palomuurisi tai virustentorjuntaohjelmasi estää pyynnön.",
|
||||
"The server (%(serverName)s) took too long to respond.": "Palvelin (%(serverName)s) ei vastannut ajoissa.",
|
||||
"Feedback sent": "Palaute lähetetty",
|
||||
"There was an error updating your community. The server is unable to process your request.": "Yhteisösi päivittämisessä tapahtui virhe. Palvelin ei voi käsitellä pyyntöäsi.",
|
||||
"You can change this later if needed.": "Voit tarvittaessa vaihtaa tämän myöhemmin.",
|
||||
"There was an error creating your community. The name may be taken or the server is unable to process your request.": "Yhteisösi luomisessa tapahtui virhe. Nimi saattaa olla varattu tai palvelin ei voi käsitellä pyyntöäsi.",
|
||||
"Download logs": "Lataa lokit",
|
||||
"Preparing to download logs": "Valmistellaan lokien lataamista",
|
||||
"About": "Tietoa",
|
||||
"Unpin": "Poista kiinnitys",
|
||||
"Customise your appearance": "Mukauta ulkoasuasi",
|
||||
"Appearance Settings only affect this %(brand)s session.": "Ulkoasuasetukset vaikuttavat vain tähän %(brand)s-istuntoon.",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Aseta käyttöjärjestelmääsi asennetun fontin nimi, niin %(brand)s pyrkii käyttämään sitä.",
|
||||
"The operation could not be completed": "Toimintoa ei voitu tehdä loppuun asti",
|
||||
"There are advanced notifications which are not shown here.": "On edistyneitä ilmoituksia, joita ei näytetä tässä.",
|
||||
"Return to call": "Palaa puheluun",
|
||||
"Voice Call": "Äänipuhelu",
|
||||
"Video Call": "Videopuhelu",
|
||||
"Send stickers to your active room as you": "Lähetä aktiiviseen huoneeseesi tarroja itsenäsi",
|
||||
"Send stickers to this room as you": "Lähetä tähän huoneeseen tarroja itsenäsi",
|
||||
"Change the avatar of your active room": "Vaihda aktiivisen huoneesi kuva",
|
||||
"Change the avatar of this room": "Vaihda huoneen kuva",
|
||||
"Change the name of your active room": "Muuta aktiivisen huoneesi nimeä",
|
||||
"Change the name of this room": "Muuta tämän huoneen nimeä",
|
||||
"Change the topic of your active room": "Muuta aktiivisen huoneesi aihetta",
|
||||
"Change the topic of this room": "Muuta huoneen aihetta",
|
||||
"Change which room you're viewing": "Vaihda näytettävää huonetta",
|
||||
"Send stickers into your active room": "Lähetä tarroja aktiiviseen huoneeseesi",
|
||||
"Send stickers into this room": "Lähetä tarroja tähän huoneeseen"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
+408
-42
@@ -42,7 +42,7 @@
|
||||
"Invite new community members": "Convidará comunidade a novos participantes",
|
||||
"Invite to Community": "Convidar á comunidade",
|
||||
"Which rooms would you like to add to this community?": "Que salas desexaría engadir a esta comunidade?",
|
||||
"Show these rooms to non-members on the community page and room list?": "Quere que estas salas se lle mostren a outros membros de fóra da comunidade na lista de salas?",
|
||||
"Show these rooms to non-members on the community page and room list?": "Queres que estas salas se lle mostren a outras participantes de fóra da comunidade e na lista de salas?",
|
||||
"Add rooms to the community": "Engadir salas á comunidade",
|
||||
"Add to community": "Engadir á comunidade",
|
||||
"Failed to invite the following users to %(groupId)s:": "Fallo ao convidar ás seguintes usuarias a %(groupId)s:",
|
||||
@@ -74,7 +74,7 @@
|
||||
"Missing user_id in request": "Falta o user_id na petición",
|
||||
"Usage": "Uso",
|
||||
"/ddg is not a command": "/ddg non é unha orde",
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "Para utilizala, agarde que carguen os resultados de autocompletado e escolla entre eles.",
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "Para utilizala, agarda a que carguen os resultados de autocompletado e escolle entre eles.",
|
||||
"Ignored user": "Usuaria ignorada",
|
||||
"You are now ignoring %(userId)s": "Agora está a ignorar %(userId)s",
|
||||
"Unignored user": "Usuarias non ignoradas",
|
||||
@@ -131,7 +131,7 @@
|
||||
"Authentication check failed: incorrect password?": "Fallou a comprobación de autenticación: contrasinal incorrecto?",
|
||||
"Failed to join room": "Non puideches entrar na sala",
|
||||
"Message Pinning": "Fixando mensaxe",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostrar marcas de tempo con formato 12 horas (ex. 2:30pm)",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostrar marcas de tempo con formato 12 horas (ex. 2:30pm)",
|
||||
"Always show message timestamps": "Mostrar sempre marcas de tempo",
|
||||
"Autoplay GIFs and videos": "Reprodución automática de GIFs e vídeos",
|
||||
"Enable automatic language detection for syntax highlighting": "Activar a detección automática de idioma para o resalte da sintaxe",
|
||||
@@ -168,8 +168,8 @@
|
||||
"Authentication": "Autenticación",
|
||||
"Last seen": "Visto por última vez",
|
||||
"Failed to set display name": "Fallo ao establecer o nome público",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
|
||||
"Mirror local video feed": "Copiar fonte de vídeo local",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
|
||||
"Mirror local video feed": "Replicar a fonte de vídeo local",
|
||||
"Cannot add any more widgets": "Non pode engadir máis trebellos",
|
||||
"The maximum permitted number of widgets have already been added to this room.": "Xa se lle engadiron o número máximo de trebellos a esta sala.",
|
||||
"Add a widget": "Engadir un trebello",
|
||||
@@ -281,9 +281,9 @@
|
||||
"Publish this room to the public in %(domain)s's room directory?": "Publicar esta sala no directorio público de salas de %(domain)s?",
|
||||
"Who can read history?": "Quen pode ler o histórico?",
|
||||
"Anyone": "Calquera",
|
||||
"Members only (since the point in time of selecting this option)": "Só membros (desde o momento en que se selecciona esta opción)",
|
||||
"Members only (since they were invited)": "Só membros (desde que foron convidados)",
|
||||
"Members only (since they joined)": "Só membros (desde que se uniron)",
|
||||
"Members only (since the point in time of selecting this option)": "Só participantes (desde o momento en que se selecciona esta opción)",
|
||||
"Members only (since they were invited)": "Só participantes (desde que foron convidadas)",
|
||||
"Members only (since they joined)": "Só participantes (desde que se uniron)",
|
||||
"Permissions": "Permisos",
|
||||
"Advanced": "Avanzado",
|
||||
"Add a topic": "Engadir asunto",
|
||||
@@ -298,7 +298,7 @@
|
||||
"You have <a>enabled</a> URL previews by default.": "<a>Activou</a> a vista previa de URL por defecto.",
|
||||
"You have <a>disabled</a> URL previews by default.": "<a>Desactivou</a> a vista previa de URL por defecto.",
|
||||
"URL previews are enabled by default for participants in this room.": "As vistas previas de URL están activas por defecto para os participantes desta sala.",
|
||||
"URL previews are disabled by default for participants in this room.": "As vistas previas de URL están desactivadas por defecto para os participantes desta sala.",
|
||||
"URL previews are disabled by default for participants in this room.": "As vistas previas de URL están desactivadas por defecto para as participantes desta sala.",
|
||||
"URL Previews": "Vista previa de URL",
|
||||
"Error decrypting audio": "Fallo ao descifrar audio",
|
||||
"Error decrypting attachment": "Fallo descifrando o anexo",
|
||||
@@ -346,7 +346,7 @@
|
||||
"Only visible to community members": "Só visible para os participantes da comunidade",
|
||||
"Filter community rooms": "Filtrar salas da comunidade",
|
||||
"Something went wrong when trying to get your communities.": "Algo fallou ao intentar obter as súas comunidades.",
|
||||
"You're not currently a member of any communities.": "Ate o momento non é membro de ningunha comunidade.",
|
||||
"You're not currently a member of any communities.": "Ate o momento non es participante en ningunha comunidade.",
|
||||
"Unknown Address": "Enderezo descoñecido",
|
||||
"Allow": "Permitir",
|
||||
"Delete Widget": "Eliminar widget",
|
||||
@@ -390,13 +390,13 @@
|
||||
"was invited %(count)s times|one": "foi convidada",
|
||||
"were banned %(count)s times|other": "foron prohibidas %(count)s veces",
|
||||
"were banned %(count)s times|one": "foron prohibidas",
|
||||
"was banned %(count)s times|other": "foi prohibida %(count)s veces",
|
||||
"was banned %(count)s times|other": "foi vetada %(count)s veces",
|
||||
"was banned %(count)s times|one": "foi prohibida",
|
||||
"were unbanned %(count)s times|other": "retiróuselle a prohibición %(count)s veces",
|
||||
"were unbanned %(count)s times|one": "retrouseille a prohibición",
|
||||
"was unbanned %(count)s times|other": "retrouseille a prohibición %(count)s veces",
|
||||
"was unbanned %(count)s times|other": "retirouselle o veto %(count)s veces",
|
||||
"was unbanned %(count)s times|one": "retiróuselle a prohibición",
|
||||
"were kicked %(count)s times|other": "foron expulsadas %(count)s veces",
|
||||
"were kicked %(count)s times|other": "foron expulsadas %(count)s veces",
|
||||
"were kicked %(count)s times|one": "foron expulsadas",
|
||||
"was kicked %(count)s times|other": "foi expulsada %(count)s veces",
|
||||
"was kicked %(count)s times|one": "foi expulsada",
|
||||
@@ -465,7 +465,7 @@
|
||||
"You must <a>register</a> to use this functionality": "Debe <a>rexistrarse</a> para utilizar esta función",
|
||||
"You must join the room to see its files": "Debes unirte a sala para ver os seus ficheiros",
|
||||
"There are no visible files in this room": "Non hai ficheiros visibles nesta sala",
|
||||
"<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 use 'img' tags\n</p>\n": "<h1>HTML para a páxina da súa comunidade</h1>\n<p>\n Utilice a descrición longa para presentar novos membros a comunidade, ou publicar algunha <a href=\"foo\">ligazón</a> importante\n \n</p>\n<p>\n Tamén pode utilizar etiquetas 'img'\n</p>\n",
|
||||
"<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 use 'img' tags\n</p>\n": "<h1>HTML para a páxina da túa comunidade</h1>\n<p>\n Utiliza a descrición longa para presentalle a comunidade ás novas participantes, ou publicar algunha <a href=\"foo\">ligazón</a> importante\n \n</p>\n<p>\n Tamén podes usar etiquetas 'img'\n</p>\n",
|
||||
"Add rooms to the community summary": "Engadir salas ao resumo da comunidade",
|
||||
"Which rooms would you like to add to this summary?": "Que salas desexa engadir a este resumo?",
|
||||
"Add to summary": "Engadir ao resumo",
|
||||
@@ -487,13 +487,13 @@
|
||||
"Leave %(groupName)s?": "Deixar %(groupName)s?",
|
||||
"Leave": "Saír",
|
||||
"Community Settings": "Axustes da comunidade",
|
||||
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Estas salas móstranse aos membros da comunidade na páxina da comunidade. Os participantes da comunidade poden unirse ás salas premendo nelas.",
|
||||
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Estas salas móstranse ás participantes da comunidade na páxina da comunidade. As participantes da comunidade poden unirse ás salas premendo nelas.",
|
||||
"Add rooms to this community": "Engadir salas a esta comunidade",
|
||||
"Featured Rooms:": "Salas destacadas:",
|
||||
"Featured Users:": "Usuarias destacadas:",
|
||||
"%(inviter)s has invited you to join this community": "%(inviter)s convidoute a entrar nesta comunidade",
|
||||
"You are an administrator of this community": "Administras esta comunidade",
|
||||
"You are a member of this community": "É membro desta comunidade",
|
||||
"You are a member of this community": "Participas nesta comunidade",
|
||||
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "A túa comunidade non ten unha descrición longa, ou unha páxina HTML que lle mostrar ás participantes.<br />Preme aquí para abrir os axustes e publicar unha!",
|
||||
"Long Description (HTML)": "Descrición longa (HTML)",
|
||||
"Description": "Descrición",
|
||||
@@ -547,14 +547,14 @@
|
||||
"<not supported>": "<non soportado>",
|
||||
"Import E2E room keys": "Importar chaves E2E da sala",
|
||||
"Cryptography": "Criptografía",
|
||||
"Analytics": "Analytics",
|
||||
"Analytics": "Análise",
|
||||
"%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s recolle información analítica anónima para permitirnos mellorar a aplicación.",
|
||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "A intimidade impórtanos, así que non recollemos información personal ou identificable nos datos dos nosos análises.",
|
||||
"Learn more about how we use analytics.": "Saber máis sobre como utilizamos analytics.",
|
||||
"Labs": "Labs",
|
||||
"Check for update": "Comprobar actualización",
|
||||
"Reject all %(invitedRooms)s invites": "Rexeitar todos os %(invitedRooms)s convites",
|
||||
"Start automatically after system login": "Iniciar automaticamente despois de iniciar sesión",
|
||||
"Start automatically after system login": "Iniciar automaticamente despois de iniciar sesión",
|
||||
"No media permissions": "Sen permisos de medios",
|
||||
"You may need to manually permit %(brand)s to access your microphone/webcam": "Igual ten que permitir manualmente a %(brand)s acceder ao seus micrófono e cámara",
|
||||
"No Microphones detected": "Non se detectaron micrófonos",
|
||||
@@ -639,11 +639,11 @@
|
||||
"Failed to add tag %(tagName)s to room": "Fallo ao engadir a etiqueta %(tagName)s a sala",
|
||||
"Key request sent.": "Petición de chave enviada.",
|
||||
"Flair": "Popularidade",
|
||||
"Showing flair for these communities:": "Mostrar a popularidade destas comunidades:",
|
||||
"Showing flair for these communities:": "Mostrando a popularidade destas comunidades:",
|
||||
"Display your community flair in rooms configured to show it.": "Mostrar a popularidade da túa comunidade nas salas configuradas para que a mostren.",
|
||||
"Did you know: you can use communities to filter your %(brand)s experience!": "Sabías que podes usar as comunidades para filtrar a túa experiencia en %(brand)s!",
|
||||
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Para establecer un filtro, arrastra un avatar da comunidade sobre o panel de filtros na parte esquerda da pantalla. Podes premer nun avatar no panel de filtrado en calquera momento para ver só salas e xente asociada a esa comunidade.",
|
||||
"Deops user with given id": "Degradar á usuaria con ese ID",
|
||||
"Deops user with given id": "Degrada usuaria co id proporcionado",
|
||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Visto por %(displayName)s(%(userName)s en %(dateTime)s",
|
||||
"Code": "Código",
|
||||
"Unable to join community": "Non te puideches unir a comunidade",
|
||||
@@ -804,8 +804,8 @@
|
||||
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Iso fará que a túa deixe de ter uso de xeito permanente. Non poderás acceder e ninguén vai a poder volver a rexistrar esa mesma ID de usuaria. Suporá que sairás de todalas salas de conversas nas que estabas e eliminarás os detalles da túa conta do servidores de identidade. <b>Esta acción non ten volta</b>",
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Desactivando a súa conta <b>non supón que por defecto esquezamos as súas mensaxes enviadas.</b> Se quere que nos esquezamos das súas mensaxes, prema na caixa de embaixo.",
|
||||
"To continue, please enter your password:": "Para continuar introduza o seu contrasinal:",
|
||||
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "A visibilidade das mensaxes en Matrix é parecida á dos correos electrónicos. Que esquezamos as túas mensaxes significa que as mensaxes non se van a compartir con ningún novo membro ou usuaria que non estea rexistrada. Mais aqueles usuarias que xa tiveron acceso a estas mensaxes si que seguirán tendo acceso as súas propias copias desas mensaxes.",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Esquezan todas as mensaxes que eu enviara no momento en que elimine a miña conta. (<b>Aviso</b>: iso suporá que os seguintes participantes só verán unha versión incompleta das conversas.)",
|
||||
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "A visibilidade das mensaxes en Matrix é parecida á dos correos electrónicos. Que esquezamos as túas mensaxes significa que as mensaxes non se van a compartir con ningunha nova participante ou usuaria que non estea rexistrada. Mais aquelas usuarias que xa tiveron acceso a estas mensaxes si que seguirán tendo acceso as súas propias copias desas mensaxes.",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Esquece todas as mensaxes que enviei cando a miña conta está desactivada. (<b>Aviso</b>: isto fará que usuarias futuras vexan unha versión incompleta das conversas)",
|
||||
"Share Room": "Compartir sala",
|
||||
"Link to most recent message": "Ligazón ás mensaxes máis recentes",
|
||||
"Share User": "Compartir usuaria",
|
||||
@@ -833,7 +833,7 @@
|
||||
"The email field must not be blank.": "Este campo de correo non pode quedar en branco.",
|
||||
"The phone number field must not be blank.": "O número de teléfono non pode quedar en branco.",
|
||||
"The password field must not be blank.": "O campo do contrasinal non pode quedar en branco.",
|
||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Non vai poder enviar mensaxes ata que revise e acepte <consentLink>os nosos termos e condicións</consentLink>.",
|
||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Non vas poder enviar mensaxes ata que revises e aceptes <consentLink>os nosos termos e condicións</consentLink>.",
|
||||
"A call is currently being placed!": "Xa se estableceu a chamada!",
|
||||
"Sorry, your homeserver is too old to participate in this room.": "Lametámolo, o seu servidor de inicio é vello de máis para participar en esta sala.",
|
||||
"Please contact your homeserver administrator.": "Por favor, contacte coa administración do seu servidor.",
|
||||
@@ -920,14 +920,14 @@
|
||||
"Messages": "Mensaxes",
|
||||
"Actions": "Accións",
|
||||
"Other": "Outro",
|
||||
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Anteponse ¯\\_(ツ)_/¯ a mensaxe en texto plano",
|
||||
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Anteponse ¯\\_(ツ)_/¯ a mensaxe en texto plano",
|
||||
"Sends a message as plain text, without interpreting it as markdown": "Envía unha mensaxe como texto plano, sen interpretalo como markdown",
|
||||
"Sends a message as html, without interpreting it as markdown": "Envía unha mensaxe como html, sen interpretalo como markdown",
|
||||
"Upgrades a room to a new version": "Subir a sala de versión",
|
||||
"You do not have the required permissions to use this command.": "Non tes os permisos suficientes para usar este comando.",
|
||||
"Error upgrading room": "Fallo ao actualizar a sala",
|
||||
"Double check that your server supports the room version chosen and try again.": "Comproba ben que o servidor soporta a versión da sala escollida e inténtao outra vez.",
|
||||
"Changes your display nickname in the current room only": "Cambia o teu nome mostrado só para esta esta sala",
|
||||
"Changes your display nickname in the current room only": "Cambia o teu nome mostrado só para esta sala",
|
||||
"Changes the avatar of the current room": "Cambia o avatar da sala actual",
|
||||
"Changes your avatar in this current room only": "Cambia o teu avatar só nesta sala",
|
||||
"Changes your avatar in all rooms": "Cambia o teu avatar en todas as salas",
|
||||
@@ -997,7 +997,7 @@
|
||||
"If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Se hai contexto que cres que axudaría a analizar o problema, como o que estabas a facer, ID da sala, ID da usuaria, etc., por favor inclúeo aquí.",
|
||||
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "Para evitar informes duplicados, mira <existingIssuesLink>os informes existentes</existingIssuesLink> primeiro (e engade un +1) ou <newIssueLink>crea un novo informe</newIssueLink> se non o atopas.",
|
||||
"Command Help": "Comando Axuda",
|
||||
"To help us prevent this in future, please <a>send us logs</a>.": "Para axudarnos a previr esto no futuro, envíanos <a>o rexistro</a>.",
|
||||
"To help us prevent this in future, please <a>send us logs</a>.": "Para axudarnos a evitar esto no futuro, envíanos <a>o rexistro</a>.",
|
||||
"Help": "Axuda",
|
||||
"Explore Public Rooms": "Explorar Salas Públicas",
|
||||
"Explore": "Explorar",
|
||||
@@ -1085,7 +1085,7 @@
|
||||
"Unrecognised address": "Enderezo non recoñecible",
|
||||
"You do not have permission to invite people to this room.": "Non tes permiso para convidar a xente a esta sala.",
|
||||
"User %(userId)s is already in the room": "A usuaria %(userId)s xa está na sala",
|
||||
"User %(user_id)s does not exist": "A usuaria %(user_id)s non existe",
|
||||
"User %(user_id)s does not exist": "A usuaria %(user_id)s non existe",
|
||||
"User %(user_id)s may or may not exist": "A usuaria %(user_id)s podería non existir",
|
||||
"The user must be unbanned before they can be invited.": "A usuria debe ser desbloqueada antes de poder convidala.",
|
||||
"Messages in this room are end-to-end encrypted.": "As mensaxes desta sala están cifradas de extremo-a-extremo.",
|
||||
@@ -1153,7 +1153,7 @@
|
||||
"Try out new ways to ignore people (experimental)": "Novos xeitos de ignorar persoas (experimental)",
|
||||
"Show join/leave messages (invites/kicks/bans unaffected)": "Mostrar mensaxes de entrada/saída (mais non convites/expulsións/bloqueos)",
|
||||
"Subscribing to a ban list will cause you to join it!": "Subscribíndote a unha lista de bloqueo fará que te unas a ela!",
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Aviso</b>: Actualizando a sala non fará que <i>os membros da sala migren automáticamente a nova versión da sala.</i> Publicaremos unha ligazón a nova sala na versión antiga da sala - os membros terán que premer na ligazón para unirse a nova sala.",
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Aviso</b>: Actualizando a sala non farás que <i>as participantes da sala migren automáticamente á nova versión da sala.</i> Publicaremos unha ligazón á nova sala na versión antiga da sala - as participantes terán que premer na ligazón para unirse a nova sala.",
|
||||
"Join the conversation with an account": "Únete a conversa cunha conta",
|
||||
"Re-join": "Volta a unirte",
|
||||
"You can only join it with a working invite.": "Só podes unirte cun convite activo.",
|
||||
@@ -1555,7 +1555,7 @@
|
||||
"Revoke": "Revogar",
|
||||
"Share": "Compartir",
|
||||
"Discovery options will appear once you have added an email above.": "As opcións de descubrimento aparecerán após ti engadas un email.",
|
||||
"Unable to revoke sharing for phone number": "Non se puido revogar a compartición do número de teléfono",
|
||||
"Unable to revoke sharing for phone number": "Non se puido revogar a compartición do número de teléfono",
|
||||
"Unable to share phone number": "Non se puido compartir o número de teléfono",
|
||||
"Unable to verify phone number.": "Non se puido verificar o número de teléfono.",
|
||||
"Please enter verification code sent via text.": "Escribe o código de verificación enviado no SMS.",
|
||||
@@ -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.",
|
||||
@@ -1776,7 +1776,7 @@
|
||||
"edited": "editada",
|
||||
"Can't load this message": "Non se cargou a mensaxe",
|
||||
"Submit logs": "Enviar rexistro",
|
||||
"Failed to load group members": "Fallou a carga dos membros do grupo",
|
||||
"Failed to load group members": "Fallou a carga das participantes do grupo",
|
||||
"Frequently Used": "Utilizado con frecuencia",
|
||||
"Smileys & People": "Sorrisos e Persoas",
|
||||
"Animals & Nature": "Animais e Natureza",
|
||||
@@ -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.",
|
||||
@@ -1884,7 +1884,7 @@
|
||||
"Message layout": "Disposición da mensaxe",
|
||||
"Compact": "Compacta",
|
||||
"Modern": "Moderna",
|
||||
"Power level": "Nivel de permisos",
|
||||
"Power level": "Nivel responsabilidade",
|
||||
"Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Verifica este dispositivo para marcalo como confiable. Confiando neste dispositivo permite que ti e outras usuarias estedes máis tranquilas ao utilizar mensaxes cifradas.",
|
||||
"Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Ao verificar este dispositivo marcaralo como confiable, e as usuarias que confiaron en ti tamén confiarán nel.",
|
||||
"Waiting for partner to confirm...": "Agardando a que o contacto confirme...",
|
||||
@@ -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!",
|
||||
@@ -2042,7 +2042,7 @@
|
||||
"Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Falta a chave pública do captcha na configuración do servidor. Informa desto á administración do teu servidor.",
|
||||
"Please review and accept all of the homeserver's policies": "Revisa e acepta todas as cláusulas do servidor",
|
||||
"Please review and accept the policies of this homeserver:": "Revisa e acepta as cláusulas deste servidor:",
|
||||
"Unable to validate homeserver/identity server": "Non se puido validar o servidor/servidor de identidade",
|
||||
"Unable to validate homeserver/identity server": "Non se puido validar o servidor de inicio/servidor de identidade",
|
||||
"Your Modular server": "O teu servidor Modular",
|
||||
"Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Escribe a localización do teu servidor Modular. Podería utilizar o teu propio nome de dominio ou ser un subdominio de <a>modular.im</a>.",
|
||||
"Server Name": "Nome do 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",
|
||||
@@ -2206,7 +2206,7 @@
|
||||
"If you don't want to set this up now, you can later in Settings.": "Se non queres configurar esto agora, pódelo facer posteriormente nos Axustes.",
|
||||
"Don't ask again": "Non preguntar outra vez",
|
||||
"New Recovery Method": "Novo Método de Recuperación",
|
||||
"A new recovery passphrase and key for Secure Messages have been detected.": "Detectouse un novo método de chave e frase de paso de recuperación para Mensaxes Seguras.",
|
||||
"A new recovery passphrase and key for Secure Messages have been detected.": "Detectouse unha nova frase de paso de recuperación e chave para Mensaxes Seguras.",
|
||||
"If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Se non configuras o novo método de recuperación, un atacante podería intentar o acceso á túa conta. Cambia inmediatamente o contrasinal da conta e configura un novo método de recuperación nos Axustes.",
|
||||
"This session is encrypting history using the new recovery method.": "Esta sesión está cifrando o historial usando o novo método de recuperación.",
|
||||
"Go to Settings": "Ir a Axustes",
|
||||
@@ -2372,7 +2372,7 @@
|
||||
"We’re excited to announce Riot is now Element!": "Emociónanos comunicar que Riot agora é Element!",
|
||||
"Learn more at <a>element.io/previously-riot</a>": "Coñece máis en <a>element.io/previously-riot</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.": "Podes usar as opcións de servidor personalizado para conectar con outros servidores Matrix indicando un URL diferente para o servidor. Poderás usar %(brand)s cunha conta Matrix existente noutro servidor de inicio.",
|
||||
"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>.": "Escribe a localización do teu servidor Element Matrix Services. Podería ser o teu propio dominio ou un subdominio de <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>.": "Escribe a localización do teu servidor Element Matrix Services. Podería ser o teu propio dominio ou un subdominio de <a>element.io</a>.",
|
||||
"Search rooms": "Buscar salas",
|
||||
"User menu": "Menú de usuaria",
|
||||
"%(brand)s Web": "Web %(brand)s",
|
||||
@@ -2453,7 +2453,7 @@
|
||||
"Privacy": "Privacidade",
|
||||
"There was an error updating your community. The server is unable to process your request.": "Algo fallou ó actualizar a comunidade. O servidor non é quen de procesar a solicitude.",
|
||||
"Update community": "Actualizar comunidade",
|
||||
"May include members not in %(communityName)s": "Podería incluir membros que non están en %(communityName)s",
|
||||
"May include members not in %(communityName)s": "Podería incluir participantes que non están en %(communityName)s",
|
||||
"Start a conversation with someone using their name, username (like <userId/>) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>.": "Iniciar unha conversa con alguén utilizando o seu nome, nome de usuaria (como <userId/>) ou enderezo de email. Esto non as convidará a %(communityName)s. Para convidar a alguén a %(communityName)s, preme <a>aquí</a>.",
|
||||
"Failed to find the general chat for this community": "Non se atopou o chat xenérico para esta comunidade",
|
||||
"Community settings": "Axustes da comunidade",
|
||||
@@ -2561,5 +2561,371 @@
|
||||
"Add comment": "Engadir comentario",
|
||||
"Please go into as much detail as you like, so we can track down the problem.": "Podes entrar en detalle canto desexes, así poderemos entender mellor o problema.",
|
||||
"%(senderName)s ended the call": "%(senderName)s finalizou a chamada",
|
||||
"You ended the call": "Finalizaches a chamada"
|
||||
"You ended the call": "Finalizaches a chamada",
|
||||
"Now, let's help you get started": "Ímosche axudar neste comezo",
|
||||
"Welcome %(name)s": "Benvida %(name)s",
|
||||
"Add a photo so people know it's you.": "Engade unha foto así a xente recoñecerate.",
|
||||
"Great, that'll help people know it's you": "Moi ben, así axudarás a que outras persoas te recoñezan",
|
||||
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Convida a persoas usando o seu nome, enderezo de email, nome de usuaria (como <userId/>) ou <a>comparte esta sala</a>.",
|
||||
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Inicia unha conversa con alguén usando o seu nome, enderezo de email ou nome de usuaria (como <userId/>).",
|
||||
"Invite by email": "Convidar por email",
|
||||
"Use the + to make a new room or explore existing ones below": "Usa o + para crear unha nova sala ou explora as existentes embaixo",
|
||||
"Offline encrypted messaging using dehydrated devices": "Mensaxería cifrada offline usando dispositivos \"deshidratados\"",
|
||||
"New version of %(brand)s is available": "Hai unha nova versión de %(brand)s dispoñible",
|
||||
"Update %(brand)s": "Actualizar %(brand)s",
|
||||
"Enable desktop notifications": "Activar notificacións de escritorio",
|
||||
"Don't miss a reply": "Non perdas as réplicas",
|
||||
"Barbados": "Barbados",
|
||||
"Bangladesh": "Bangladesh",
|
||||
"Bahrain": "Bahrain",
|
||||
"Bahamas": "Bahamas",
|
||||
"Azerbaijan": "Azerbaixán",
|
||||
"Austria": "Austria",
|
||||
"Australia": "Australia",
|
||||
"Aruba": "Aruba",
|
||||
"Armenia": "Armenia",
|
||||
"Argentina": "Arxentina",
|
||||
"Antigua & Barbuda": "Antigua & Barbuda",
|
||||
"Antarctica": "Antártida",
|
||||
"Anguilla": "Anguilla",
|
||||
"Angola": "Angola",
|
||||
"Andorra": "Andorra",
|
||||
"American Samoa": "Samoa americana",
|
||||
"Algeria": "Alxeria",
|
||||
"Albania": "Albania",
|
||||
"Åland Islands": "Åland Islands",
|
||||
"Afghanistan": "Afghanistán",
|
||||
"United States": "EEUU de América",
|
||||
"United Kingdom": "Reino Unido",
|
||||
"Ecuador": "Ecuador",
|
||||
"Dominican Republic": "República Dominicana",
|
||||
"Dominica": "Dominica",
|
||||
"Djibouti": "Djibouti",
|
||||
"Denmark": "Dinamarca",
|
||||
"Côte d’Ivoire": "Costa de Marfil",
|
||||
"Czech Republic": "República Checa",
|
||||
"Cyprus": "Chipre",
|
||||
"Curaçao": "Curaçao",
|
||||
"Cuba": "Cuba",
|
||||
"Croatia": "Croacia",
|
||||
"Costa Rica": "Costa Rica",
|
||||
"Cook Islands": "Cook Islands",
|
||||
"Congo - Kinshasa": "Congo - Kinshasa",
|
||||
"Congo - Brazzaville": "Congo - Brazzaville",
|
||||
"Comoros": "Comoros",
|
||||
"Colombia": "Colombia",
|
||||
"Cocos (Keeling) Islands": "Cocos (Keeling) Islands",
|
||||
"Christmas Island": "Christmas Island",
|
||||
"China": "China",
|
||||
"Chile": "Chile",
|
||||
"Chad": "Chad",
|
||||
"Central African Republic": "República Centroafricana",
|
||||
"Cayman Islands": "Illas caimán",
|
||||
"Caribbean Netherlands": "Paises baixos do Caribe",
|
||||
"Cape Verde": "Cabo Verde",
|
||||
"Canada": "Canadá",
|
||||
"Cameroon": "Camerún",
|
||||
"Cambodia": "Cambodia",
|
||||
"Burundi": "Burundi",
|
||||
"Burkina Faso": "Burkina Faso",
|
||||
"Bulgaria": "Bulgaria",
|
||||
"Brunei": "Brunei",
|
||||
"British Virgin Islands": "British Virgin Islands",
|
||||
"British Indian Ocean Territory": "British Indian Ocean Territory",
|
||||
"Brazil": "Brasil",
|
||||
"Bouvet Island": "Illa Bouvet",
|
||||
"Botswana": "Botswana",
|
||||
"Bosnia": "Bosnia",
|
||||
"Bolivia": "Bolivia",
|
||||
"Bhutan": "Bután",
|
||||
"Bermuda": "Bermuda",
|
||||
"Benin": "Benín",
|
||||
"Belize": "Belice",
|
||||
"Belgium": "Bélxica",
|
||||
"Belarus": "Belarús",
|
||||
"Places the call in the current room on hold": "Pon en pausa a chamada da sala actual",
|
||||
"Takes the call in the current room off hold": "Acepta a chamada na sala actual",
|
||||
"%(creator)s created this DM.": "%(creator)s creou esta MD.",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "As mensaxes desta sala están cifradas de extremo-a-extremo. Cando se unan, podes verificar as persoas no seu perfil, tocando no seu avatar.",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Aquí as mensaxes están cifradas de extremo-a-extre. Verifica a %(displayName)s no seu perfil - toca no seu avatar.",
|
||||
"Role": "Rol",
|
||||
"This is the start of <roomName/>.": "Este é o comezo de <roomName/>.",
|
||||
"Add a photo, so people can easily spot your room.": "Engade unha foto para que se poida identificar a sala facilmente.",
|
||||
"%(displayName)s created this room.": "%(displayName)s creou esta sala.",
|
||||
"You created this room.": "Creaches esta sala.",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Engade un tema</a> para axudarlle á xente que coñeza de que trata.",
|
||||
"Topic: %(topic)s ": "Asunto: %(topic)s ",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Asunto: %(topic)s (<a>editar</a>)",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "Este é o comezo do teu historial de conversa con <displayName/>.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Só vós as dúas estades nesta conversa, a non ser que convidedes a alguén máis.",
|
||||
"Call Paused": "Chamada en pausa",
|
||||
"Zimbabwe": "Zimbabue",
|
||||
"Zambia": "Zambia",
|
||||
"Yemen": "Yemen",
|
||||
"Western Sahara": "Sahara Occidental",
|
||||
"Wallis & Futuna": "Wallis & Futuna",
|
||||
"Vietnam": "Vietnam",
|
||||
"Venezuela": "Venezuela",
|
||||
"Vatican City": "Cidade do Vaticano",
|
||||
"Vanuatu": "Vanuatu",
|
||||
"Uzbekistan": "Uzbekistan",
|
||||
"Uruguay": "Uruguai",
|
||||
"United Arab Emirates": "Emiratos Árabes Unidos",
|
||||
"Ukraine": "Ucraína",
|
||||
"Uganda": "Uganda",
|
||||
"U.S. Virgin Islands": "U.S. Virgin Islands",
|
||||
"Tuvalu": "Tuvalu",
|
||||
"Turks & Caicos Islands": "Turks & Caicos Islands",
|
||||
"Turkmenistan": "Turkmenistán",
|
||||
"Turkey": "Turquía",
|
||||
"Tunisia": "Túnez",
|
||||
"Trinidad & Tobago": "Trinidad & Tobago",
|
||||
"Tonga": "Tonga",
|
||||
"Tokelau": "Tokelau",
|
||||
"Togo": "Togo",
|
||||
"Timor-Leste": "Timor-Leste",
|
||||
"Thailand": "Tailandia",
|
||||
"Tanzania": "Tanzania",
|
||||
"Tajikistan": "Tajikistan",
|
||||
"Taiwan": "Taiwan",
|
||||
"São Tomé & Príncipe": "Santo Tomé e Príncipe",
|
||||
"Syria": "Siria",
|
||||
"Switzerland": "Suiza",
|
||||
"Sweden": "Suecia",
|
||||
"Swaziland": "Suazilandia",
|
||||
"Svalbard & Jan Mayen": "Svalbard & Jan Mayen",
|
||||
"Suriname": "Surinam",
|
||||
"Sudan": "Sudán",
|
||||
"St. Vincent & Grenadines": "St. Vincent & Grenadines",
|
||||
"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": "España",
|
||||
"South Sudan": "Sudán do Sur",
|
||||
"South Korea": "Corea do Sur",
|
||||
"South Georgia & South Sandwich Islands": "Illas South Georgia & South Sandwich",
|
||||
"South Africa": "África do Sur",
|
||||
"Somalia": "Somalia",
|
||||
"Solomon Islands": "Illas Salomón",
|
||||
"Slovenia": "Eslovenia",
|
||||
"Slovakia": "Eslovaquia",
|
||||
"Sint Maarten": "Sint Maarten",
|
||||
"Singapore": "Singapur",
|
||||
"Sierra Leone": "Serra Leona",
|
||||
"Seychelles": "Seichelles",
|
||||
"Serbia": "Serbia",
|
||||
"Senegal": "Senegal",
|
||||
"Saudi Arabia": "Arabia Saudita",
|
||||
"San Marino": "San Marino",
|
||||
"Samoa": "Samoa",
|
||||
"Réunion": "Reunión",
|
||||
"Rwanda": "Ruanda",
|
||||
"Russia": "Rusia",
|
||||
"Romania": "Romanía",
|
||||
"Qatar": "Catar",
|
||||
"Puerto Rico": "Porto Rico",
|
||||
"Portugal": "Portugal",
|
||||
"Poland": "Polonia",
|
||||
"Pitcairn Islands": "Illas Pitcairn",
|
||||
"Philippines": "Filipinas",
|
||||
"Peru": "Perú",
|
||||
"Paraguay": "Paraguai",
|
||||
"Papua New Guinea": "Papua Nova Guinea",
|
||||
"Panama": "Panamá",
|
||||
"Palestine": "Palestina",
|
||||
"Palau": "Palau",
|
||||
"Pakistan": "Paquistán",
|
||||
"Oman": "Omán",
|
||||
"Norway": "Noruega",
|
||||
"Northern Mariana Islands": "Illas Marianas do Norte",
|
||||
"North Korea": "Corea do Norte",
|
||||
"Norfolk Island": "Illa Norfolk",
|
||||
"Niue": "Niue",
|
||||
"Nigeria": "Nixeria",
|
||||
"Niger": "Níxer",
|
||||
"Nicaragua": "Nicaragua",
|
||||
"New Zealand": "Nova Zelanda",
|
||||
"New Caledonia": "Nova Caledonia",
|
||||
"Netherlands": "Paises Baixos",
|
||||
"Nepal": "Nepal",
|
||||
"Nauru": "Nauru",
|
||||
"Namibia": "Namibia",
|
||||
"Myanmar": "Myanmar",
|
||||
"Mozambique": "Mozambique",
|
||||
"Morocco": "Marrocos",
|
||||
"Montserrat": "Montserrat",
|
||||
"Montenegro": "Montenegro",
|
||||
"Mongolia": "Mongolia",
|
||||
"Monaco": "Mónaco",
|
||||
"Moldova": "Moldavia",
|
||||
"Micronesia": "Micronesia",
|
||||
"Mexico": "México",
|
||||
"Mayotte": "Mayotte",
|
||||
"Mauritius": "Mauricio",
|
||||
"Mauritania": "Mauritania",
|
||||
"Martinique": "Martinica",
|
||||
"Marshall Islands": "Illas Marshall",
|
||||
"Malta": "Malta",
|
||||
"Mali": "Mali",
|
||||
"Maldives": "Maldivas",
|
||||
"Malaysia": "Malaisia",
|
||||
"Malawi": "Malawi",
|
||||
"Madagascar": "Madagascar",
|
||||
"Macedonia": "Macedonia",
|
||||
"Macau": "Macau",
|
||||
"Luxembourg": "Luxemburgo",
|
||||
"Lithuania": "Lituania",
|
||||
"Liechtenstein": "Liechtenstein",
|
||||
"Libya": "Libia",
|
||||
"Liberia": "Liberia",
|
||||
"Lesotho": "Lesoto",
|
||||
"Lebanon": "Líbano",
|
||||
"Latvia": "Letonia",
|
||||
"Laos": "Laos",
|
||||
"Kyrgyzstan": "Kyrgyzstan",
|
||||
"Kuwait": "Kuwait",
|
||||
"Kosovo": "Kosovo",
|
||||
"Kiribati": "Kiribati",
|
||||
"Kenya": "Kenia",
|
||||
"Kazakhstan": "Kazakhstan",
|
||||
"Jordan": "Xordania",
|
||||
"Jersey": "Jersey",
|
||||
"Japan": "Xapón",
|
||||
"Jamaica": "Xamaica",
|
||||
"Italy": "Italia",
|
||||
"Israel": "Israel",
|
||||
"Isle of Man": "Illa de Man",
|
||||
"Ireland": "Irlanda",
|
||||
"Iraq": "Iraq",
|
||||
"Iran": "Irán",
|
||||
"Indonesia": "Indonesia",
|
||||
"India": "India",
|
||||
"Iceland": "Islandia",
|
||||
"Hungary": "Hungría",
|
||||
"Hong Kong": "Hong Kong",
|
||||
"Honduras": "Honduras",
|
||||
"Heard & McDonald Islands": "Heard & McDonald Islands",
|
||||
"Haiti": "Haiti",
|
||||
"Guyana": "Guyana",
|
||||
"Guinea-Bissau": "Guinea-Bissau",
|
||||
"Guinea": "Guinea",
|
||||
"Guernsey": "Guernsey",
|
||||
"Guatemala": "Guatemala",
|
||||
"Guam": "Goam",
|
||||
"Guadeloupe": "Guadalupe",
|
||||
"Grenada": "Granada",
|
||||
"Greenland": "Groenlandia",
|
||||
"Greece": "Grecia",
|
||||
"Gibraltar": "Xibraltar",
|
||||
"Ghana": "Ghana",
|
||||
"Germany": "Alemaña",
|
||||
"Georgia": "Xeorxia",
|
||||
"Gambia": "Gambia",
|
||||
"Gabon": "Gabón",
|
||||
"French Southern Territories": "French Southern Territories",
|
||||
"French Polynesia": "Polinesia francesa",
|
||||
"French Guiana": "Guaiana Francesa",
|
||||
"France": "Francia",
|
||||
"Finland": "Finlandia",
|
||||
"Fiji": "Fiji",
|
||||
"Faroe Islands": "Illas Feroe",
|
||||
"Falkland Islands": "Illas Falkland",
|
||||
"Ethiopia": "Etiopía",
|
||||
"Estonia": "Estonia",
|
||||
"Eritrea": "Eritrea",
|
||||
"Equatorial Guinea": "Guinea Ecuatorial",
|
||||
"El Salvador": "O Salvador",
|
||||
"Egypt": "Exipto",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)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 %(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": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(count)s salas.",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
|
||||
+431
-134
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user