Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/feat/widgets
This commit is contained in:
@@ -17,9 +17,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
|
||||
import EventIndexPeg from './indexing/EventIndexPeg';
|
||||
import createMatrixClient from './utils/createMatrixClient';
|
||||
import Analytics from './Analytics';
|
||||
@@ -47,44 +50,46 @@ import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
||||
interface ILoadSessionOpts {
|
||||
enableGuest?: boolean;
|
||||
guestHsUrl?: string;
|
||||
guestIsUrl?: string;
|
||||
ignoreGuest?: boolean;
|
||||
defaultDeviceDisplayName?: string;
|
||||
fragmentQueryParams?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
* a number of things:
|
||||
*
|
||||
*
|
||||
* 1. if we have a guest access token in the fragment query params, it uses
|
||||
* that.
|
||||
*
|
||||
* 2. if an access token is stored in local storage (from a previous session),
|
||||
* it uses that.
|
||||
*
|
||||
* 3. it attempts to auto-register as a guest user.
|
||||
*
|
||||
* If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in
|
||||
* turn will raise on_logged_in and will_start_client events.
|
||||
*
|
||||
* @param {object} opts
|
||||
*
|
||||
* @param {object} opts.fragmentQueryParams: string->string map of the
|
||||
* @param {object} [opts]
|
||||
* @param {object} [opts.fragmentQueryParams]: string->string map of the
|
||||
* query-parameters extracted from the #-fragment of the starting URI.
|
||||
*
|
||||
* @param {boolean} opts.enableGuest: set to true to enable guest access tokens
|
||||
* and auto-guest registrations.
|
||||
*
|
||||
* @params {string} opts.guestHsUrl: homeserver URL. Only used if enableGuest is
|
||||
* true; defines the HS to register against.
|
||||
*
|
||||
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
|
||||
* true; defines the IS to use.
|
||||
*
|
||||
* @params {bool} opts.ignoreGuest: If the stored session is a guest account, ignore
|
||||
* it and don't load it.
|
||||
*
|
||||
* @param {boolean} [opts.enableGuest]: set to true to enable guest access
|
||||
* tokens and auto-guest registrations.
|
||||
* @param {string} [opts.guestHsUrl]: homeserver URL. Only used if enableGuest
|
||||
* is true; defines the HS to register against.
|
||||
* @param {string} [opts.guestIsUrl]: homeserver URL. Only used if enableGuest
|
||||
* is true; defines the IS to use.
|
||||
* @param {bool} [opts.ignoreGuest]: If the stored session is a guest account,
|
||||
* ignore it and don't load it.
|
||||
* @param {string} [opts.defaultDeviceDisplayName]: Default display name to use
|
||||
* when registering as a guest.
|
||||
* @returns {Promise} a promise which resolves when the above process completes.
|
||||
* Resolves to `true` if we ended up starting a session, or `false` if we
|
||||
* failed.
|
||||
*/
|
||||
export async function loadSession(opts) {
|
||||
export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean> {
|
||||
try {
|
||||
let enableGuest = opts.enableGuest || false;
|
||||
const guestHsUrl = opts.guestHsUrl;
|
||||
@@ -97,12 +102,13 @@ export async function loadSession(opts) {
|
||||
enableGuest = false;
|
||||
}
|
||||
|
||||
if (enableGuest &&
|
||||
if (
|
||||
enableGuest &&
|
||||
fragmentQueryParams.guest_user_id &&
|
||||
fragmentQueryParams.guest_access_token
|
||||
) {
|
||||
) {
|
||||
console.log("Using guest access credentials");
|
||||
return _doSetLoggedIn({
|
||||
return doSetLoggedIn({
|
||||
userId: fragmentQueryParams.guest_user_id,
|
||||
accessToken: fragmentQueryParams.guest_access_token,
|
||||
homeserverUrl: guestHsUrl,
|
||||
@@ -110,7 +116,7 @@ export async function loadSession(opts) {
|
||||
guest: true,
|
||||
}, true).then(() => true);
|
||||
}
|
||||
const success = await _restoreFromLocalStorage({
|
||||
const success = await restoreFromLocalStorage({
|
||||
ignoreGuest: Boolean(opts.ignoreGuest),
|
||||
});
|
||||
if (success) {
|
||||
@@ -118,7 +124,7 @@ export async function loadSession(opts) {
|
||||
}
|
||||
|
||||
if (enableGuest) {
|
||||
return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
|
||||
return registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
|
||||
}
|
||||
|
||||
// fall back to welcome screen
|
||||
@@ -129,7 +135,7 @@ export async function loadSession(opts) {
|
||||
// need to show the general failure dialog. Instead, just go back to welcome.
|
||||
return false;
|
||||
}
|
||||
return _handleLoadSessionFailure(e);
|
||||
return handleLoadSessionFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +145,7 @@ export async function loadSession(opts) {
|
||||
* is associated with them. The session is not loaded.
|
||||
* @returns {String} The persisted session's owner, if an owner exists. Null otherwise.
|
||||
*/
|
||||
export function getStoredSessionOwner() {
|
||||
export function getStoredSessionOwner(): string {
|
||||
const {hsUrl, userId, accessToken} = getLocalStorageSessionVars();
|
||||
return hsUrl && userId && accessToken ? userId : null;
|
||||
}
|
||||
@@ -148,7 +154,7 @@ export function getStoredSessionOwner() {
|
||||
* @returns {bool} True if the stored session is for a guest user or false if it is
|
||||
* for a real user. If there is no stored session, return null.
|
||||
*/
|
||||
export function getStoredSessionIsGuest() {
|
||||
export function getStoredSessionIsGuest(): boolean {
|
||||
const sessVars = getLocalStorageSessionVars();
|
||||
return sessVars.hsUrl && sessVars.userId && sessVars.accessToken ? sessVars.isGuest : null;
|
||||
}
|
||||
@@ -163,7 +169,10 @@ export function getStoredSessionIsGuest() {
|
||||
* @returns {Promise} promise which resolves to true if we completed the token
|
||||
* login, else false
|
||||
*/
|
||||
export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
||||
export function attemptTokenLogin(
|
||||
queryParams: Record<string, string>,
|
||||
defaultDeviceDisplayName?: string,
|
||||
): Promise<boolean> {
|
||||
if (!queryParams.loginToken) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
@@ -184,10 +193,10 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
||||
},
|
||||
).then(function(creds) {
|
||||
console.log("Logged in with token");
|
||||
return _clearStorage().then(() => {
|
||||
_persistCredentialsToLocalStorage(creds);
|
||||
return clearStorage().then(() => {
|
||||
persistCredentialsToLocalStorage(creds);
|
||||
// remember that we just logged in
|
||||
sessionStorage.setItem("mx_fresh_login", true);
|
||||
sessionStorage.setItem("mx_fresh_login", String(true));
|
||||
return true;
|
||||
});
|
||||
}).catch((err) => {
|
||||
@@ -197,8 +206,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
||||
});
|
||||
}
|
||||
|
||||
export function handleInvalidStoreError(e) {
|
||||
if (e.reason === Matrix.InvalidStoreError.TOGGLED_LAZY_LOADING) {
|
||||
export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
|
||||
if (e.reason === InvalidStoreError.TOGGLED_LAZY_LOADING) {
|
||||
return Promise.resolve().then(() => {
|
||||
const lazyLoadEnabled = e.value;
|
||||
if (lazyLoadEnabled) {
|
||||
@@ -231,7 +240,11 @@ export function handleInvalidStoreError(e) {
|
||||
}
|
||||
}
|
||||
|
||||
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
function registerAsGuest(
|
||||
hsUrl: string,
|
||||
isUrl: string,
|
||||
defaultDeviceDisplayName: string,
|
||||
): Promise<boolean> {
|
||||
console.log(`Doing guest login on ${hsUrl}`);
|
||||
|
||||
// create a temporary MatrixClient to do the login
|
||||
@@ -245,7 +258,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
},
|
||||
}).then((creds) => {
|
||||
console.log(`Registered as guest: ${creds.user_id}`);
|
||||
return _doSetLoggedIn({
|
||||
return doSetLoggedIn({
|
||||
userId: creds.user_id,
|
||||
deviceId: creds.device_id,
|
||||
accessToken: creds.access_token,
|
||||
@@ -259,12 +272,21 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
});
|
||||
}
|
||||
|
||||
export interface ILocalStorageSession {
|
||||
hsUrl: string;
|
||||
isUrl: string;
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
isGuest: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about the stored session in localstorage. The session
|
||||
* may not be valid, as it is not tested for consistency here.
|
||||
* @returns {Object} Information about the session - see implementation for variables.
|
||||
*/
|
||||
export function getLocalStorageSessionVars() {
|
||||
export function getLocalStorageSessionVars(): ILocalStorageSession {
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
const accessToken = localStorage.getItem("mx_access_token");
|
||||
@@ -292,8 +314,8 @@ export function getLocalStorageSessionVars() {
|
||||
// The plan is to gradually move the localStorage access done here into
|
||||
// SessionStore to avoid bugs where the view becomes out-of-sync with
|
||||
// localStorage (e.g. isGuest etc.)
|
||||
async function _restoreFromLocalStorage(opts) {
|
||||
const ignoreGuest = opts.ignoreGuest;
|
||||
async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
|
||||
const ignoreGuest = opts?.ignoreGuest;
|
||||
|
||||
if (!localStorage) {
|
||||
return false;
|
||||
@@ -314,11 +336,11 @@ async function _restoreFromLocalStorage(opts) {
|
||||
console.log("No pickle key available");
|
||||
}
|
||||
|
||||
const freshLogin = sessionStorage.getItem("mx_fresh_login");
|
||||
const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true";
|
||||
sessionStorage.removeItem("mx_fresh_login");
|
||||
|
||||
console.log(`Restoring session for ${userId}`);
|
||||
await _doSetLoggedIn({
|
||||
await doSetLoggedIn({
|
||||
userId: userId,
|
||||
deviceId: deviceId,
|
||||
accessToken: accessToken,
|
||||
@@ -335,7 +357,7 @@ async function _restoreFromLocalStorage(opts) {
|
||||
}
|
||||
}
|
||||
|
||||
async function _handleLoadSessionFailure(e) {
|
||||
async function handleLoadSessionFailure(e: Error): Promise<boolean> {
|
||||
console.error("Unable to load session", e);
|
||||
|
||||
const SessionRestoreErrorDialog =
|
||||
@@ -348,7 +370,7 @@ async function _handleLoadSessionFailure(e) {
|
||||
const [success] = await modal.finished;
|
||||
if (success) {
|
||||
// user clicked continue.
|
||||
await _clearStorage();
|
||||
await clearStorage();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -369,12 +391,12 @@ async function _handleLoadSessionFailure(e) {
|
||||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
export async function setLoggedIn(credentials) {
|
||||
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
||||
credentials.freshLogin = true;
|
||||
stopMatrixClient();
|
||||
const pickleKey = credentials.userId && credentials.deviceId
|
||||
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
|
||||
: null;
|
||||
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
|
||||
: null;
|
||||
|
||||
if (pickleKey) {
|
||||
console.log("Created pickle key");
|
||||
@@ -382,7 +404,7 @@ export async function setLoggedIn(credentials) {
|
||||
console.log("Pickle key not created");
|
||||
}
|
||||
|
||||
return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
|
||||
return doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,7 +422,7 @@ export async function setLoggedIn(credentials) {
|
||||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
export function hydrateSession(credentials) {
|
||||
export function hydrateSession(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
||||
const oldUserId = MatrixClientPeg.get().getUserId();
|
||||
const oldDeviceId = MatrixClientPeg.get().getDeviceId();
|
||||
|
||||
@@ -413,7 +435,7 @@ export function hydrateSession(credentials) {
|
||||
console.warn("Clearing all data: Old session belongs to a different user/session");
|
||||
}
|
||||
|
||||
return _doSetLoggedIn(credentials, overwrite);
|
||||
return doSetLoggedIn(credentials, overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,7 +447,10 @@ export function hydrateSession(credentials) {
|
||||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
async function doSetLoggedIn(
|
||||
credentials: IMatrixClientCreds,
|
||||
clearStorageEnabled: boolean,
|
||||
): Promise<MatrixClient> {
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
|
||||
const softLogout = isSoftLogout();
|
||||
@@ -448,8 +473,8 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
// (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
|
||||
dis.dispatch({action: 'on_logging_in'}, true);
|
||||
|
||||
if (clearStorage) {
|
||||
await _clearStorage();
|
||||
if (clearStorageEnabled) {
|
||||
await clearStorage();
|
||||
}
|
||||
|
||||
const results = await StorageManager.checkConsistency();
|
||||
@@ -457,9 +482,9 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
// crypto store, we'll be generally confused when handling encrypted data.
|
||||
// Show a modal recommending a full reset of storage.
|
||||
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
|
||||
const signOut = await _showStorageEvictedDialog();
|
||||
const signOut = await showStorageEvictedDialog();
|
||||
if (signOut) {
|
||||
await _clearStorage();
|
||||
await clearStorage();
|
||||
// This error feels a bit clunky, but we want to make sure we don't go any
|
||||
// further and instead head back to sign in.
|
||||
throw new AbortLoginAndRebuildStorage(
|
||||
@@ -487,20 +512,9 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
|
||||
if (localStorage) {
|
||||
try {
|
||||
_persistCredentialsToLocalStorage(credentials);
|
||||
|
||||
persistCredentialsToLocalStorage(credentials);
|
||||
// make sure we don't think that it's a fresh login any more
|
||||
sessionStorage.removeItem("mx_fresh_login");
|
||||
|
||||
// The user registered as a PWLU (PassWord-Less User), the generated password
|
||||
// is cached here such that the user can change it at a later time.
|
||||
if (credentials.password) {
|
||||
// Update SessionStore
|
||||
dis.dispatch({
|
||||
action: 'cached_password',
|
||||
cachedPassword: credentials.password,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Error using local storage: can't persist session!", e);
|
||||
}
|
||||
@@ -514,7 +528,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
return client;
|
||||
}
|
||||
|
||||
function _showStorageEvictedDialog() {
|
||||
function showStorageEvictedDialog(): Promise<boolean> {
|
||||
const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog');
|
||||
return new Promise(resolve => {
|
||||
Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, {
|
||||
@@ -527,7 +541,7 @@ function _showStorageEvictedDialog() {
|
||||
// `instanceof`. Babel 7 supports this natively in their class handling.
|
||||
class AbortLoginAndRebuildStorage extends Error { }
|
||||
|
||||
function _persistCredentialsToLocalStorage(credentials) {
|
||||
function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void {
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
|
||||
if (credentials.identityServerUrl) {
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
|
||||
@@ -537,7 +551,7 @@ function _persistCredentialsToLocalStorage(credentials) {
|
||||
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
|
||||
|
||||
if (credentials.pickleKey) {
|
||||
localStorage.setItem("mx_has_pickle_key", true);
|
||||
localStorage.setItem("mx_has_pickle_key", String(true));
|
||||
} else {
|
||||
if (localStorage.getItem("mx_has_pickle_key")) {
|
||||
console.error("Expected a pickle key, but none provided. Encryption may not work.");
|
||||
@@ -561,7 +575,7 @@ let _isLoggingOut = false;
|
||||
/**
|
||||
* Logs the current session out and transitions to the logged-out state
|
||||
*/
|
||||
export function logout() {
|
||||
export function logout(): void {
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
@@ -590,7 +604,7 @@ export function logout() {
|
||||
);
|
||||
}
|
||||
|
||||
export function softLogout() {
|
||||
export function softLogout(): void {
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
|
||||
// Track that we've detected and trapped a soft logout. This helps prevent other
|
||||
@@ -611,11 +625,11 @@ export function softLogout() {
|
||||
// DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
|
||||
}
|
||||
|
||||
export function isSoftLogout() {
|
||||
export function isSoftLogout(): boolean {
|
||||
return localStorage.getItem("mx_soft_logout") === "true";
|
||||
}
|
||||
|
||||
export function isLoggingOut() {
|
||||
export function isLoggingOut(): boolean {
|
||||
return _isLoggingOut;
|
||||
}
|
||||
|
||||
@@ -625,7 +639,7 @@ export function isLoggingOut() {
|
||||
* @param {boolean} startSyncing True (default) to actually start
|
||||
* syncing the client.
|
||||
*/
|
||||
async function startMatrixClient(startSyncing=true) {
|
||||
async function startMatrixClient(startSyncing = true): Promise<void> {
|
||||
console.log(`Lifecycle: Starting MatrixClient`);
|
||||
|
||||
// dispatch this before starting the matrix client: it's used
|
||||
@@ -684,21 +698,21 @@ async function startMatrixClient(startSyncing=true) {
|
||||
* Stops a running client and all related services, and clears persistent
|
||||
* storage. Used after a session has been logged out.
|
||||
*/
|
||||
export async function onLoggedOut() {
|
||||
export async function onLoggedOut(): Promise<void> {
|
||||
_isLoggingOut = false;
|
||||
// Ensure that we dispatch a view change **before** stopping the client so
|
||||
// so that React components unmount first. This avoids React soft crashes
|
||||
// that can occur when components try to use a null client.
|
||||
dis.dispatch({action: 'on_logged_out'}, true);
|
||||
stopMatrixClient();
|
||||
await _clearStorage({deleteEverything: true});
|
||||
await clearStorage({deleteEverything: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} opts Options for how to clear storage.
|
||||
* @returns {Promise} promise which resolves once the stores have been cleared
|
||||
*/
|
||||
async function _clearStorage(opts: {deleteEverything: boolean}) {
|
||||
async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void> {
|
||||
Analytics.disable();
|
||||
|
||||
if (window.localStorage) {
|
||||
@@ -736,7 +750,7 @@ async function _clearStorage(opts: {deleteEverything: boolean}) {
|
||||
* @param {boolean} unsetClient True (default) to abandon the client
|
||||
* on MatrixClientPeg after stopping.
|
||||
*/
|
||||
export function stopMatrixClient(unsetClient=true) {
|
||||
export function stopMatrixClient(unsetClient = true): void {
|
||||
Notifier.stop();
|
||||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
||||
@@ -18,35 +18,72 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||
import Matrix from "matrix-js-sdk";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
||||
|
||||
interface ILoginOptions {
|
||||
defaultDeviceDisplayName?: string;
|
||||
}
|
||||
|
||||
// TODO: Move this to JS SDK
|
||||
interface ILoginFlow {
|
||||
type: string;
|
||||
}
|
||||
|
||||
// TODO: Move this to JS SDK
|
||||
/* eslint-disable camelcase */
|
||||
interface ILoginParams {
|
||||
identifier?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
device_id?: string;
|
||||
initial_device_display_name?: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export default class Login {
|
||||
constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
|
||||
this._hsUrl = hsUrl;
|
||||
this._isUrl = isUrl;
|
||||
this._fallbackHsUrl = fallbackHsUrl;
|
||||
this._currentFlowIndex = 0;
|
||||
this._flows = [];
|
||||
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
this._tempClient = null; // memoize
|
||||
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 defaultDeviceDisplayName: string;
|
||||
private tempClient: MatrixClient;
|
||||
|
||||
constructor(
|
||||
hsUrl: string,
|
||||
isUrl: string,
|
||||
fallbackHsUrl?: string,
|
||||
opts?: ILoginOptions,
|
||||
) {
|
||||
this.hsUrl = hsUrl;
|
||||
this.isUrl = isUrl;
|
||||
this.fallbackHsUrl = fallbackHsUrl;
|
||||
this.currentFlowIndex = 0;
|
||||
this.flows = [];
|
||||
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
this.tempClient = null; // memoize
|
||||
}
|
||||
|
||||
getHomeserverUrl() {
|
||||
return this._hsUrl;
|
||||
public getHomeserverUrl(): string {
|
||||
return this.hsUrl;
|
||||
}
|
||||
|
||||
getIdentityServerUrl() {
|
||||
return this._isUrl;
|
||||
public getIdentityServerUrl(): string {
|
||||
return this.isUrl;
|
||||
}
|
||||
|
||||
setHomeserverUrl(hsUrl) {
|
||||
this._tempClient = null; // clear memoization
|
||||
this._hsUrl = hsUrl;
|
||||
public setHomeserverUrl(hsUrl: string): void {
|
||||
this.tempClient = null; // clear memoization
|
||||
this.hsUrl = hsUrl;
|
||||
}
|
||||
|
||||
setIdentityServerUrl(isUrl) {
|
||||
this._tempClient = null; // clear memoization
|
||||
this._isUrl = isUrl;
|
||||
public setIdentityServerUrl(isUrl: string): void {
|
||||
this.tempClient = null; // clear memoization
|
||||
this.isUrl = isUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,40 +91,41 @@ export default class Login {
|
||||
* requests.
|
||||
* @returns {MatrixClient}
|
||||
*/
|
||||
createTemporaryClient() {
|
||||
if (this._tempClient) return this._tempClient; // use memoization
|
||||
return this._tempClient = Matrix.createClient({
|
||||
baseUrl: this._hsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
public createTemporaryClient(): MatrixClient {
|
||||
if (this.tempClient) return this.tempClient; // use memoization
|
||||
return this.tempClient = Matrix.createClient({
|
||||
baseUrl: this.hsUrl,
|
||||
idBaseUrl: this.isUrl,
|
||||
});
|
||||
}
|
||||
|
||||
getFlows() {
|
||||
const self = this;
|
||||
public async getFlows(): Promise<Array<ILoginFlow>> {
|
||||
const client = this.createTemporaryClient();
|
||||
return client.loginFlows().then(function(result) {
|
||||
self._flows = result.flows;
|
||||
self._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 self._flows;
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
chooseFlow(flowIndex) {
|
||||
this._currentFlowIndex = flowIndex;
|
||||
public chooseFlow(flowIndex): void {
|
||||
this.currentFlowIndex = flowIndex;
|
||||
}
|
||||
|
||||
getCurrentFlowStep() {
|
||||
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];
|
||||
const flowStep = this.flows[this.currentFlowIndex];
|
||||
return flowStep ? flowStep.type : null;
|
||||
}
|
||||
|
||||
loginViaPassword(username, phoneCountry, phoneNumber, pass) {
|
||||
const self = this;
|
||||
|
||||
public loginViaPassword(
|
||||
username: string,
|
||||
phoneCountry: string,
|
||||
phoneNumber: string,
|
||||
password: string,
|
||||
): Promise<IMatrixClientCreds> {
|
||||
const isEmail = username.indexOf("@") > 0;
|
||||
|
||||
let identifier;
|
||||
@@ -113,14 +151,14 @@ export default class Login {
|
||||
}
|
||||
|
||||
const loginParams = {
|
||||
password: pass,
|
||||
identifier: identifier,
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
password,
|
||||
identifier,
|
||||
initial_device_display_name: this.defaultDeviceDisplayName,
|
||||
};
|
||||
|
||||
const tryFallbackHs = (originalError) => {
|
||||
return sendLoginRequest(
|
||||
self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams,
|
||||
this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams,
|
||||
).catch((fallbackError) => {
|
||||
console.log("fallback HS login failed", fallbackError);
|
||||
// throw the original error
|
||||
@@ -130,11 +168,11 @@ export default class Login {
|
||||
|
||||
let originalLoginError = null;
|
||||
return sendLoginRequest(
|
||||
self._hsUrl, self._isUrl, 'm.login.password', loginParams,
|
||||
this.hsUrl, this.isUrl, 'm.login.password', loginParams,
|
||||
).catch((error) => {
|
||||
originalLoginError = error;
|
||||
if (error.httpStatus === 403) {
|
||||
if (self._fallbackHsUrl) {
|
||||
if (this.fallbackHsUrl) {
|
||||
return tryFallbackHs(originalLoginError);
|
||||
}
|
||||
}
|
||||
@@ -154,11 +192,16 @@ export default class Login {
|
||||
* @param {string} hsUrl the base url of the Homeserver used to log in.
|
||||
* @param {string} isUrl the base url of the default identity server
|
||||
* @param {string} loginType the type of login to do
|
||||
* @param {object} loginParams the parameters for the login
|
||||
* @param {ILoginParams} loginParams the parameters for the login
|
||||
*
|
||||
* @returns {MatrixClientCreds}
|
||||
*/
|
||||
export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
|
||||
export async function sendLoginRequest(
|
||||
hsUrl: string,
|
||||
isUrl: string,
|
||||
loginType: string,
|
||||
loginParams: ILoginParams,
|
||||
): Promise<IMatrixClientCreds> {
|
||||
const client = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
@@ -38,9 +38,9 @@ export interface IMatrixClientCreds {
|
||||
homeserverUrl: string;
|
||||
identityServerUrl: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
deviceId?: string;
|
||||
accessToken: string;
|
||||
guest: boolean;
|
||||
guest?: boolean;
|
||||
pickleKey?: string;
|
||||
freshLogin?: boolean;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
// import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
|
||||
// Regex for what a "safe" or "Matrix-looking" localpart would be.
|
||||
// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514
|
||||
@@ -44,70 +43,27 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
|
||||
*/
|
||||
export async function startAnyRegistrationFlow(options) {
|
||||
if (options === undefined) options = {};
|
||||
// look for an ILAG compatible flow. We define this as one
|
||||
// which has only dummy or recaptcha flows. In practice it
|
||||
// would support any stage InteractiveAuth supports, just not
|
||||
// ones like email & msisdn which require the user to supply
|
||||
// the relevant details in advance. We err on the side of
|
||||
// caution though.
|
||||
|
||||
// XXX: ILAG is disabled for now,
|
||||
// see https://github.com/vector-im/element-web/issues/8222
|
||||
|
||||
// const flows = await _getRegistrationFlows();
|
||||
// const hasIlagFlow = flows.some((flow) => {
|
||||
// return flow.stages.every((stage) => {
|
||||
// return ['m.login.dummy', 'm.login.recaptcha', 'm.login.terms'].includes(stage);
|
||||
// });
|
||||
// });
|
||||
|
||||
// if (hasIlagFlow) {
|
||||
// dis.dispatch({
|
||||
// action: 'view_set_mxid',
|
||||
// go_home_on_cancel: options.go_home_on_cancel,
|
||||
// });
|
||||
//} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||
hasCancelButton: true,
|
||||
quitOnly: true,
|
||||
title: _t("Sign In or Create Account"),
|
||||
description: _t("Use your account or create a new one to continue."),
|
||||
button: _t("Create Account"),
|
||||
extraButtons: [
|
||||
<button key="start_login" onClick={() => {
|
||||
modal.close();
|
||||
dis.dispatch({action: 'start_login', screenAfterLogin: options.screen_after});
|
||||
}}>{ _t('Sign In') }</button>,
|
||||
],
|
||||
onFinished: (proceed) => {
|
||||
if (proceed) {
|
||||
dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after});
|
||||
} else if (options.go_home_on_cancel) {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else if (options.go_welcome_on_cancel) {
|
||||
dis.dispatch({action: 'view_welcome_page'});
|
||||
}
|
||||
},
|
||||
});
|
||||
//}
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||
hasCancelButton: true,
|
||||
quitOnly: true,
|
||||
title: _t("Sign In or Create Account"),
|
||||
description: _t("Use your account or create a new one to continue."),
|
||||
button: _t("Create Account"),
|
||||
extraButtons: [
|
||||
<button key="start_login" onClick={() => {
|
||||
modal.close();
|
||||
dis.dispatch({action: 'start_login', screenAfterLogin: options.screen_after});
|
||||
}}>{ _t('Sign In') }</button>,
|
||||
],
|
||||
onFinished: (proceed) => {
|
||||
if (proceed) {
|
||||
dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after});
|
||||
} else if (options.go_home_on_cancel) {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else if (options.go_welcome_on_cancel) {
|
||||
dis.dispatch({action: 'view_welcome_page'});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// async function _getRegistrationFlows() {
|
||||
// try {
|
||||
// await MatrixClientPeg.get().register(
|
||||
// null,
|
||||
// null,
|
||||
// undefined,
|
||||
// {},
|
||||
// {},
|
||||
// );
|
||||
// console.log("Register request succeeded when it should have returned 401!");
|
||||
// } catch (e) {
|
||||
// if (e.httpStatus === 401) {
|
||||
// return e.data.flows;
|
||||
// }
|
||||
// throw e;
|
||||
// }
|
||||
// throw new Error("Register request succeeded when it should have returned 401!");
|
||||
// }
|
||||
|
||||
@@ -27,7 +27,6 @@ import CallMediaHandler from '../../CallMediaHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
@@ -41,10 +40,6 @@ import HomePage from "./HomePage";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { DefaultTagID } from "../../stores/room-list/models";
|
||||
import {
|
||||
showToast as showSetPasswordToast,
|
||||
hideToast as hideSetPasswordToast,
|
||||
} from "../../toasts/SetPasswordToast";
|
||||
import {
|
||||
showToast as showServerLimitToast,
|
||||
hideToast as hideServerLimitToast,
|
||||
@@ -149,8 +144,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
protected readonly _matrixClient: MatrixClient;
|
||||
protected readonly _roomView: React.RefObject<any>;
|
||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||
protected readonly _sessionStore: sessionStore;
|
||||
protected readonly _sessionStoreToken: { remove: () => void };
|
||||
protected readonly _compactLayoutWatcherRef: string;
|
||||
protected resizer: Resizer;
|
||||
|
||||
@@ -171,12 +164,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
|
||||
this._sessionStore = sessionStore;
|
||||
this._sessionStoreToken = this._sessionStore.addListener(
|
||||
this._setStateFromSessionStore,
|
||||
);
|
||||
this._setStateFromSessionStore();
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
@@ -205,9 +192,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
SettingsStore.unwatchSetting(this._compactLayoutWatcherRef);
|
||||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
this.resizer.detach();
|
||||
}
|
||||
|
||||
@@ -228,14 +212,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
return this._roomView.current.canResetTimeline();
|
||||
};
|
||||
|
||||
_setStateFromSessionStore = () => {
|
||||
if (this._sessionStore.getCachedPassword()) {
|
||||
showSetPasswordToast();
|
||||
} else {
|
||||
hideSetPasswordToast();
|
||||
}
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
const classNames = {
|
||||
handle: "mx_ResizeHandle",
|
||||
|
||||
@@ -30,7 +30,7 @@ import 'what-input';
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import * as RoomListSorter from "../../RoomListSorter";
|
||||
@@ -290,7 +290,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
// When the session loads it'll be detected as soft logged out and a dispatch
|
||||
// will be sent out to say that, triggering this MatrixChat to show the soft
|
||||
// logout page.
|
||||
Lifecycle.loadSession({});
|
||||
Lifecycle.loadSession();
|
||||
}
|
||||
|
||||
this.accountPassword = null;
|
||||
@@ -670,9 +670,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
case 'view_home_page':
|
||||
this.viewHome();
|
||||
break;
|
||||
case 'view_set_mxid':
|
||||
this.setMxId(payload);
|
||||
break;
|
||||
case 'view_start_chat_or_reuse':
|
||||
this.chatCreateOrReuse(payload.user_id);
|
||||
break;
|
||||
@@ -985,36 +982,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
});
|
||||
}
|
||||
|
||||
private setMxId(payload) {
|
||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
|
||||
onFinished: (submitted, credentials) => {
|
||||
if (!submitted) {
|
||||
dis.dispatch({
|
||||
action: 'cancel_after_sync_prepared',
|
||||
});
|
||||
if (payload.go_home_on_cancel) {
|
||||
dis.dispatch({
|
||||
action: 'view_home_page',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
MatrixClientPeg.setJustRegisteredUserId(credentials.user_id);
|
||||
this.onRegistered(credentials);
|
||||
},
|
||||
onDifferentServerClicked: (ev) => {
|
||||
dis.dispatch({action: 'start_registration'});
|
||||
close();
|
||||
},
|
||||
onLoginClick: (ev) => {
|
||||
dis.dispatch({action: 'start_login'});
|
||||
close();
|
||||
},
|
||||
}).close;
|
||||
}
|
||||
|
||||
private async createRoom(defaultPublic = false) {
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
if (communityId) {
|
||||
@@ -1814,12 +1781,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.showScreen("forgot_password");
|
||||
};
|
||||
|
||||
onRegisterFlowComplete = (credentials: object, password: string) => {
|
||||
onRegisterFlowComplete = (credentials: IMatrixClientCreds, password: string) => {
|
||||
return this.onUserCompletedLoginFlow(credentials, password);
|
||||
};
|
||||
|
||||
// returns a promise which resolves to the new MatrixClient
|
||||
onRegistered(credentials: object) {
|
||||
onRegistered(credentials: IMatrixClientCreds) {
|
||||
return Lifecycle.setLoggedIn(credentials);
|
||||
}
|
||||
|
||||
@@ -1905,7 +1872,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
* Note: SSO users (and any others using token login) currently do not pass through
|
||||
* this, as they instead jump straight into the app after `attemptTokenLogin`.
|
||||
*/
|
||||
onUserCompletedLoginFlow = async (credentials: object, password: string) => {
|
||||
onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string) => {
|
||||
this.accountPassword = password;
|
||||
// self-destruct the password after 5mins
|
||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||
|
||||
@@ -1090,42 +1090,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
room_id: this.getRoomId(),
|
||||
},
|
||||
});
|
||||
|
||||
// Don't peek whilst registering otherwise getPendingEventList complains
|
||||
// Do this by indicating our intention to join
|
||||
|
||||
// XXX: ILAG is disabled for now,
|
||||
// see https://github.com/vector-im/element-web/issues/8222
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
// dis.dispatch({
|
||||
// action: 'will_join',
|
||||
// });
|
||||
|
||||
// const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||
// const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||
// homeserverUrl: cli.getHomeserverUrl(),
|
||||
// onFinished: (submitted, credentials) => {
|
||||
// if (submitted) {
|
||||
// this.props.onRegistered(credentials);
|
||||
// } else {
|
||||
// dis.dispatch({
|
||||
// action: 'cancel_after_sync_prepared',
|
||||
// });
|
||||
// dis.dispatch({
|
||||
// action: 'cancel_join',
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// onDifferentServerClicked: (ev) => {
|
||||
// dis.dispatch({action: 'start_registration'});
|
||||
// close();
|
||||
// },
|
||||
// onLoginClick: (ev) => {
|
||||
// dis.dispatch({action: 'start_login'});
|
||||
// close();
|
||||
// },
|
||||
// }).close;
|
||||
// return;
|
||||
} else {
|
||||
Promise.resolve().then(() => {
|
||||
const signUrl = this.props.threepidInvite?.signUrl;
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations 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, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import { Key } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
|
||||
// The amount of time to wait for further changes to the input username before
|
||||
// sending a request to the server
|
||||
const USERNAME_CHECK_DEBOUNCE_MS = 250;
|
||||
|
||||
/*
|
||||
* Prompt the user to set a display name.
|
||||
*
|
||||
* On success, `onFinished(true, newDisplayName)` is called.
|
||||
*/
|
||||
export default class SetMxIdDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
// Called when the user requests to register with a different homeserver
|
||||
onDifferentServerClicked: PropTypes.func.isRequired,
|
||||
// Called if the user wants to switch to login instead
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._input_value = createRef();
|
||||
this._uiAuth = createRef();
|
||||
|
||||
this.state = {
|
||||
// The entered username
|
||||
username: '',
|
||||
// Indicate ongoing work on the username
|
||||
usernameBusy: false,
|
||||
// Indicate error with username
|
||||
usernameError: '',
|
||||
// Assume the homeserver supports username checking until "M_UNRECOGNIZED"
|
||||
usernameCheckSupport: true,
|
||||
|
||||
// Whether the auth UI is currently being used
|
||||
doingUIAuth: false,
|
||||
// Indicate error with auth
|
||||
authError: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._input_value.current.select();
|
||||
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
onValueChange = ev => {
|
||||
this.setState({
|
||||
username: ev.target.value,
|
||||
usernameBusy: true,
|
||||
usernameError: '',
|
||||
}, () => {
|
||||
if (!this.state.username || !this.state.usernameCheckSupport) {
|
||||
this.setState({
|
||||
usernameBusy: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce the username check to limit number of requests sent
|
||||
if (this._usernameCheckTimeout) {
|
||||
clearTimeout(this._usernameCheckTimeout);
|
||||
}
|
||||
this._usernameCheckTimeout = setTimeout(() => {
|
||||
this._doUsernameCheck().finally(() => {
|
||||
this.setState({
|
||||
usernameBusy: false,
|
||||
});
|
||||
});
|
||||
}, USERNAME_CHECK_DEBOUNCE_MS);
|
||||
});
|
||||
};
|
||||
|
||||
onKeyUp = ev => {
|
||||
if (ev.key === Key.ENTER) {
|
||||
this.onSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = ev => {
|
||||
if (this._uiAuth.current) {
|
||||
this._uiAuth.current.tryContinue();
|
||||
}
|
||||
this.setState({
|
||||
doingUIAuth: true,
|
||||
});
|
||||
};
|
||||
|
||||
_doUsernameCheck() {
|
||||
// We do a quick check ahead of the username availability API to ensure the
|
||||
// user ID roughly looks okay from a Matrix perspective.
|
||||
if (!SAFE_LOCALPART_REGEX.test(this.state.username)) {
|
||||
this.setState({
|
||||
usernameError: _t("A username can only contain lower case letters, numbers and '=_-./'"),
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
// Check if username is available
|
||||
return this._matrixClient.isUsernameAvailable(this.state.username).then(
|
||||
(isAvailable) => {
|
||||
if (isAvailable) {
|
||||
this.setState({usernameError: ''});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
// Indicate whether the homeserver supports username checking
|
||||
const newState = {
|
||||
usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED",
|
||||
};
|
||||
console.error('Error whilst checking username availability: ', err);
|
||||
switch (err.errcode) {
|
||||
case "M_USER_IN_USE":
|
||||
newState.usernameError = _t('Username not available');
|
||||
break;
|
||||
case "M_INVALID_USERNAME":
|
||||
newState.usernameError = _t(
|
||||
'Username invalid: %(errMessage)s',
|
||||
{ errMessage: err.message},
|
||||
);
|
||||
break;
|
||||
case "M_UNRECOGNIZED":
|
||||
// This homeserver doesn't support username checking, assume it's
|
||||
// fine and rely on the error appearing in registration step.
|
||||
newState.usernameError = '';
|
||||
break;
|
||||
case undefined:
|
||||
newState.usernameError = _t('Something went wrong!');
|
||||
break;
|
||||
default:
|
||||
newState.usernameError = _t(
|
||||
'An error occurred: %(error_string)s',
|
||||
{ error_string: err.message },
|
||||
);
|
||||
break;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_generatePassword() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
_makeRegisterRequest = auth => {
|
||||
// Not upgrading - changing mxids
|
||||
const guestAccessToken = null;
|
||||
if (!this._generatedPassword) {
|
||||
this._generatedPassword = this._generatePassword();
|
||||
}
|
||||
return this._matrixClient.register(
|
||||
this.state.username,
|
||||
this._generatedPassword,
|
||||
undefined, // session id: included in the auth dict already
|
||||
auth,
|
||||
{},
|
||||
guestAccessToken,
|
||||
);
|
||||
};
|
||||
|
||||
_onUIAuthFinished = (success, response) => {
|
||||
this.setState({
|
||||
doingUIAuth: false,
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
this.setState({ authError: response.message });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onFinished(true, {
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token,
|
||||
password: this._generatedPassword,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
|
||||
let auth;
|
||||
if (this.state.doingUIAuth) {
|
||||
auth = <InteractiveAuth
|
||||
matrixClient={this._matrixClient}
|
||||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={{}}
|
||||
poll={true}
|
||||
ref={this._uiAuth}
|
||||
continueIsManaged={true}
|
||||
/>;
|
||||
}
|
||||
const inputClasses = classnames({
|
||||
"mx_SetMxIdDialog_input": true,
|
||||
"error": Boolean(this.state.usernameError),
|
||||
});
|
||||
|
||||
let usernameIndicator = null;
|
||||
if (this.state.usernameBusy) {
|
||||
usernameIndicator = <div>{_t("Checking...")}</div>;
|
||||
} else {
|
||||
const usernameAvailable = this.state.username &&
|
||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||
const usernameIndicatorClasses = classnames({
|
||||
"error": Boolean(this.state.usernameError),
|
||||
"success": usernameAvailable,
|
||||
});
|
||||
usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
|
||||
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
|
||||
</div>;
|
||||
}
|
||||
|
||||
let authErrorIndicator = null;
|
||||
if (this.state.authError) {
|
||||
authErrorIndicator = <div className="error" role="alert">
|
||||
{ this.state.authError }
|
||||
</div>;
|
||||
}
|
||||
const canContinue = this.state.username &&
|
||||
!this.state.usernameError &&
|
||||
!this.state.usernameBusy;
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetMxIdDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('To get started, please pick a username!')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<div className="mx_SetMxIdDialog_input_group">
|
||||
<input type="text" ref={this._input_value} value={this.state.username}
|
||||
autoFocus={true}
|
||||
onChange={this.onValueChange}
|
||||
onKeyUp={this.onKeyUp}
|
||||
size="30"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
{ _t(
|
||||
'This will be your account name on the <span></span> ' +
|
||||
'homeserver, or you can pick a <a>different server</a>.',
|
||||
{},
|
||||
{
|
||||
'span': <span>{ this.props.homeserverUrl }</span>,
|
||||
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
{ auth }
|
||||
{ authErrorIndicator }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input className="mx_Dialog_primary"
|
||||
type="submit"
|
||||
value={_t("Continue")}
|
||||
onClick={this.onSubmit}
|
||||
disabled={!canContinue}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
const WarmFuzzy = function(props) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
let title = _t('You have successfully set a password!');
|
||||
if (props.didSetEmail) {
|
||||
title = _t('You have successfully set a password and an email address!');
|
||||
}
|
||||
const advice = _t('You can now return to your account after signing out, and sign in on other devices.');
|
||||
let extraAdvice = null;
|
||||
if (!props.didSetEmail) {
|
||||
extraAdvice = _t('Remember, you can always set an email address in user settings if you change your mind.');
|
||||
}
|
||||
|
||||
return <BaseDialog className="mx_SetPasswordDialog"
|
||||
onFinished={props.onFinished}
|
||||
title={ title }
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ advice }
|
||||
</p>
|
||||
<p>
|
||||
{ extraAdvice }
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
className="mx_Dialog_primary"
|
||||
autoFocus={true}
|
||||
onClick={props.onFinished}>
|
||||
{ _t('Continue') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt the user to set a password
|
||||
*
|
||||
* On success, `onFinished()` when finished
|
||||
*/
|
||||
export default class SetPasswordDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
_onPasswordChanged = res => {
|
||||
Modal.createDialog(WarmFuzzy, {
|
||||
didSetEmail: res.didSetEmail,
|
||||
onFinished: () => {
|
||||
this.props.onFinished();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
_onPasswordChangeError = err => {
|
||||
let errMsg = err.error || "";
|
||||
if (err.httpStatus === 403) {
|
||||
errMsg = _t('Failed to change password. Is your password correct?');
|
||||
} else if (err.httpStatus) {
|
||||
errMsg += ' ' + _t(
|
||||
'(HTTP status %(httpStatus)s)',
|
||||
{ httpStatus: err.httpStatus },
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
error: errMsg,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const ChangePassword = sdk.getComponent('views.settings.ChangePassword');
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetPasswordDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={ _t('Please set a password!') }
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t('This will allow you to return to your account after signing out, and sign in on other sessions.') }
|
||||
</p>
|
||||
<ChangePassword
|
||||
className="mx_SetPasswordDialog_change_password"
|
||||
rowClassName=""
|
||||
buttonClassNames="mx_Dialog_primary mx_SetPasswordDialog_change_password_button"
|
||||
buttonKind="primary"
|
||||
confirm={false}
|
||||
autoFocusNewPasswordInput={true}
|
||||
shouldAskForEmail={true}
|
||||
onError={this._onPasswordChangeError}
|
||||
onFinished={this._onPasswordChanged}
|
||||
buttonLabel={_t("Set Password")}
|
||||
/>
|
||||
<div className="error">
|
||||
{ this.state.error }
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,8 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
||||
};
|
||||
|
||||
render() {
|
||||
const {title, tooltip, children, tooltipClassName, ...props} = this.props;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {title, tooltip, children, tooltipClassName, forceHide, ...props} = this.props;
|
||||
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
|
||||
@@ -401,7 +401,8 @@ export default class TextualBody extends React.Component {
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
||||
const stripReply = ReplyThread.getParentEventId(mxEvent);
|
||||
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
|
||||
const stripReply = !mxEvent.replacingEvent() && ReplyThread.getParentEventId(mxEvent);
|
||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||
disableBigEmoji: content.msgtype === "m.emote" || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
|
||||
// Part of Replies fallback support
|
||||
|
||||
@@ -437,6 +437,7 @@ export default class MessageComposer extends React.Component {
|
||||
const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
controls.push(
|
||||
<HangupButton
|
||||
key="controls_hangup"
|
||||
roomId={this.props.room.roomId}
|
||||
isConference={true}
|
||||
canEndConference={canEndConf}
|
||||
|
||||
@@ -19,14 +19,12 @@ import Field from "../elements/Field";
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
|
||||
import sessionStore from '../../../stores/SessionStore';
|
||||
|
||||
export default class ChangePassword extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func,
|
||||
@@ -66,33 +64,11 @@ export default class ChangePassword extends React.Component {
|
||||
|
||||
state = {
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
cachedPassword: null,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._sessionStore = sessionStore;
|
||||
this._sessionStoreToken = this._sessionStore.addListener(
|
||||
this._setStateFromSessionStore,
|
||||
);
|
||||
|
||||
this._setStateFromSessionStore();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
}
|
||||
|
||||
_setStateFromSessionStore = () => {
|
||||
this.setState({
|
||||
cachedPassword: this._sessionStore.getCachedPassword(),
|
||||
});
|
||||
};
|
||||
|
||||
changePassword(oldPassword, newPassword) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
@@ -119,8 +95,11 @@ export default class ChangePassword extends React.Component {
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
<button
|
||||
key="exportRoomKeys"
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}
|
||||
>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>,
|
||||
],
|
||||
@@ -150,9 +129,6 @@ export default class ChangePassword extends React.Component {
|
||||
});
|
||||
|
||||
cli.setPassword(authDict, newPassword).then(() => {
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({action: 'password_changed'});
|
||||
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this._optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
@@ -212,7 +188,7 @@ export default class ChangePassword extends React.Component {
|
||||
|
||||
onClickChange = (ev) => {
|
||||
ev.preventDefault();
|
||||
const oldPassword = this.state.cachedPassword || this.state.oldPassword;
|
||||
const oldPassword = this.state.oldPassword;
|
||||
const newPassword = this.state.newPassword;
|
||||
const confirmPassword = this.state.newPasswordConfirm;
|
||||
const err = this.props.onCheckPassword(
|
||||
@@ -231,31 +207,22 @@ export default class ChangePassword extends React.Component {
|
||||
const rowClassName = this.props.rowClassName;
|
||||
const buttonClassName = this.props.buttonClassName;
|
||||
|
||||
let currentPassword = null;
|
||||
if (!this.state.cachedPassword) {
|
||||
currentPassword = (
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
type="password"
|
||||
label={_t('Current password')}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (this.state.phase) {
|
||||
case ChangePassword.Phases.Edit:
|
||||
const passwordLabel = this.state.cachedPassword ?
|
||||
_t('Password') : _t('New Password');
|
||||
return (
|
||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
{ currentPassword }
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
type="password"
|
||||
label={passwordLabel}
|
||||
label={_t('Current password')}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
type="password"
|
||||
label={_t('New Password')}
|
||||
value={this.state.newPassword}
|
||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||
onChange={this.onChangeNewPassword}
|
||||
@@ -277,10 +244,9 @@ export default class ChangePassword extends React.Component {
|
||||
</form>
|
||||
);
|
||||
case ChangePassword.Phases.Uploading:
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<div className="mx_Dialog_content">
|
||||
<Loader />
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"Continue": "Fortfahren",
|
||||
"Create Room": "Raum erstellen",
|
||||
"Cryptography": "Verschlüsselung",
|
||||
"Deactivate Account": "Benutzerkonto schließen",
|
||||
"Deactivate Account": "Benutzerkonto deaktivieren",
|
||||
"Failed to send email": "Fehler beim Senden der E-Mail",
|
||||
"Account": "Benutzerkonto",
|
||||
"Click here to fix": "Zum reparieren hier klicken",
|
||||
@@ -1322,7 +1322,7 @@
|
||||
"Add Email Address": "E-Mail-Adresse hinzufügen",
|
||||
"Add Phone Number": "Telefonnummer hinzufügen",
|
||||
"Changes the avatar of the current room": "Ändert den Avatar für diesen Raum",
|
||||
"Deactivate account": "Benutzerkonto schließen",
|
||||
"Deactivate account": "Benutzerkonto deaktivieren",
|
||||
"Show previews/thumbnails for images": "Zeige Vorschauen/Thumbnails für Bilder",
|
||||
"View": "Vorschau",
|
||||
"Find a room…": "Suche einen Raum…",
|
||||
@@ -2504,5 +2504,20 @@
|
||||
"Start a conversation with someone using their name or username (like <userId/>).": "Starte ein Gespräch unter Verwendung des Namen oder Benutzernamens des Gegenübers (z. B. <userId/>).",
|
||||
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Das wird sie nicht zu %(communityName)s einladen. Um jemand zu %(communityName)s einzuladen, klicke <a>hier</a>",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Lade jemand mittels seinem/ihrem Namen oder Benutzernamen (z.B. <userId/>) ein, oder <a>teile diesem Raum</a>.",
|
||||
"Unable to set up keys": "Schlüssel können nicht eingerichtet werden"
|
||||
"Unable to set up keys": "Schlüssel können nicht eingerichtet werden",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Nutze die <a>Desktop-App</a> um alle verschlüsselten Dateien zu sehen",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "Nutze die <a>Desktop-App</a> um verschlüsselte Nachrichten zu suchen",
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Diese Version von %(brand)s unterstützt nicht alle verschlüsselten Dateien anzuzeigen",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Diese Version von %(brand)s unterstützt nicht verschlüsselte Nachrichten zu durchsuchen",
|
||||
"Cannot create rooms in this community": "Räume können in dieser Community nicht erstellt werden",
|
||||
"You do not have permission to create rooms in this community.": "Du bist nicht berechtigt Räume in dieser Community zu erstellen.",
|
||||
"End conference": "Konferenzgespräch beenden",
|
||||
"This will end the conference for everyone. Continue?": "Dies wird das Konferenzgespräch für alle beenden. Fortfahren?",
|
||||
"Join the conference at the top of this room": "Konferenzgespräch oben in diesem Raum beitreten",
|
||||
"Join the conference from the room information card on the right": "Konferenzgespräch in den Rauminformationen rechts beitreten",
|
||||
"Video conference ended by %(senderName)s": "Videokonferenz von %(senderName)s beendet",
|
||||
"Video conference updated by %(senderName)s": "Videokonferenz wurde von %(senderName)s aktualisiert",
|
||||
"Video conference started by %(senderName)s": "Videokonferenz wurde von %(senderName)s gestartet",
|
||||
"Ignored attempt to disable encryption": "Versuch, die Verschlüsselung zu deaktivieren, wurde ignoriert",
|
||||
"Failed to save your profile": "Profil speichern fehlgeschlagen"
|
||||
}
|
||||
|
||||
@@ -401,9 +401,6 @@
|
||||
"Contact your <a>server admin</a>.": "Contact your <a>server admin</a>.",
|
||||
"Warning": "Warning",
|
||||
"Ok": "Ok",
|
||||
"Set password": "Set password",
|
||||
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
|
||||
"Set Password": "Set Password",
|
||||
"Set up Secure Backup": "Set up Secure Backup",
|
||||
"Encryption upgrade available": "Encryption upgrade available",
|
||||
"Verify this session": "Verify this session",
|
||||
@@ -636,7 +633,6 @@
|
||||
"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",
|
||||
"Password": "Password",
|
||||
"New Password": "New Password",
|
||||
"Confirm password": "Confirm password",
|
||||
"Change Password": "Change Password",
|
||||
@@ -1817,22 +1813,6 @@
|
||||
"Verification Pending": "Verification Pending",
|
||||
"Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.",
|
||||
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
|
||||
"A username can only contain lower case letters, numbers and '=_-./'": "A username can only contain lower case letters, numbers and '=_-./'",
|
||||
"Username not available": "Username not available",
|
||||
"Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s",
|
||||
"An error occurred: %(error_string)s": "An error occurred: %(error_string)s",
|
||||
"Checking...": "Checking...",
|
||||
"Username available": "Username available",
|
||||
"To get started, please pick a username!": "To get started, please pick a username!",
|
||||
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.",
|
||||
"If you already have a Matrix account you can <a>log in</a> instead.": "If you already have a Matrix account you can <a>log in</a> instead.",
|
||||
"You have successfully set a password!": "You have successfully set a password!",
|
||||
"You have successfully set a password and an email address!": "You have successfully set a password and an email address!",
|
||||
"You can now return to your account after signing out, and sign in on other devices.": "You can now return to your account after signing out, and sign in on other devices.",
|
||||
"Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.",
|
||||
"(HTTP status %(httpStatus)s)": "(HTTP status %(httpStatus)s)",
|
||||
"Please set a password!": "Please set a password!",
|
||||
"This will allow you to return to your account after signing out, and sign in on other sessions.": "This will allow you to return to your account after signing out, and sign in on other sessions.",
|
||||
"Share Room": "Share Room",
|
||||
"Link to most recent message": "Link to most recent message",
|
||||
"Share User": "Share User",
|
||||
@@ -1946,6 +1926,7 @@
|
||||
"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:",
|
||||
|
||||
@@ -2512,5 +2512,17 @@
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "See %(brand)s versioon ei toeta mõnede krüptitud failide vaatatamist",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "See %(brand)s versioon ei toeta otsingut krüptitud sõnumite seast",
|
||||
"Cannot create rooms in this community": "Siia kogukonda ei saa jututubasid luua",
|
||||
"You do not have permission to create rooms in this community.": "Sul pole õigusi luua siin kogukonnas uusi jututubasid."
|
||||
"You do not have permission to create rooms in this community.": "Sul pole õigusi luua siin kogukonnas uusi jututubasid.",
|
||||
"Join the conference at the top of this room": "Liitu konverentsiga selle jututoa ülaosas",
|
||||
"Join the conference from the room information card on the right": "Liitu konverentsiga selle jututoa infolehelt paremal",
|
||||
"Video conference ended by %(senderName)s": "%(senderName)s lõpetas video rühmakõne",
|
||||
"Video conference updated by %(senderName)s": "%(senderName)s uuendas video rühmakõne",
|
||||
"Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõne",
|
||||
"End conference": "Lõpeta videokonverents",
|
||||
"This will end the conference for everyone. Continue?": "Sellega lõpetame kõikide osalejate jaoks videokonverentsi. Nõus?",
|
||||
"Ignored attempt to disable encryption": "Eirasin katset lõpetada krüptimise kasutamine",
|
||||
"Offline encrypted messaging using dehydrated devices": "Võrguühenduseta kasutamiseks mõeldud krüptitud sõnumid dehydrated teenuse abil",
|
||||
"Remove messages sent by others": "Kustuta teiste saadetud sõnumid",
|
||||
"Failed to save your profile": "Sinu profiili salvestamine ei õnnestunud",
|
||||
"The operation could not be completed": "Toimingut ei õnnestunud lõpetada"
|
||||
}
|
||||
|
||||
@@ -2512,5 +2512,16 @@
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Esta versión de %(brand)s non soporta o visionado dalgúns ficheiros cifrados",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Esta versión de %(brand)s non soporta a busca de mensaxes cifradas",
|
||||
"Cannot create rooms in this community": "Non se poden crear salas nesta comunidade",
|
||||
"You do not have permission to create rooms in this community.": "Non tes permiso para crear salas nesta comunidade."
|
||||
"You do not have permission to create rooms in this community.": "Non tes permiso para crear salas nesta comunidade.",
|
||||
"Join the conference at the top of this room": "Únete á conferencia na ligazón arriba nesta sala",
|
||||
"Join the conference from the room information card on the right": "Únete á conferencia desde a tarxeta con información da sala á dereita",
|
||||
"Video conference ended by %(senderName)s": "Video conferencia rematada por %(senderName)s",
|
||||
"Video conference updated by %(senderName)s": "Video conferencia actualizada por %(senderName)s",
|
||||
"Video conference started by %(senderName)s": "Video conferencia iniciada por %(senderName)s",
|
||||
"End conference": "Rematar conferencia",
|
||||
"This will end the conference for everyone. Continue?": "Así finalizarás a conferencia para todas. ¿Adiante?",
|
||||
"Ignored attempt to disable encryption": "Intento ignorado de desactivar o cifrado",
|
||||
"Failed to save your profile": "Non se gardaron os cambios",
|
||||
"The operation could not be completed": "Non se puido realizar a acción",
|
||||
"Remove messages sent by others": "Eliminar mensaxes enviadas por outras"
|
||||
}
|
||||
|
||||
@@ -2502,5 +2502,27 @@
|
||||
"Minimize widget": "Widget minimalizálása",
|
||||
"Maximize widget": "Widget maximalizálása",
|
||||
"Your server requires encryption to be enabled in private rooms.": "A szervered megköveteli, hogy a titkosítás be legyen kapcsolva a privát szobákban.",
|
||||
"Unable to set up keys": "Nem sikerült a kulcsok beállítása"
|
||||
"Unable to set up keys": "Nem sikerült a kulcsok beállítása",
|
||||
"Safeguard against losing access to encrypted messages & data": "Biztosítás a titkosított üzenetek és adatokhoz való hozzáférés elvesztése ellen",
|
||||
"not found in storage": "a tárban nem található",
|
||||
"Widgets": "Kisalkalmazások",
|
||||
"Edit widgets, bridges & bots": "Kisalkalmazások, hidak és botok szerkesztése",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Minden titkosított fájl eléréséhez használd az <a>Asztali alkalmazást</a>",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "A titkosított üzenetek kereséséhez használd az <a>Asztali alkalmazást</a>",
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "%(brand)s ezen verziója nem minden titkosított fájl megjelenítését támogatja",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "%(brand)s ezen verziója nem támogatja a keresést a titkosított üzenetekben",
|
||||
"Cannot create rooms in this community": "A közösségben nem lehet szobát készíteni",
|
||||
"You do not have permission to create rooms in this community.": "A közösségben szoba létrehozásához nincs jogosultságod.",
|
||||
"End conference": "Konferenciahívás befejezése",
|
||||
"This will end the conference for everyone. Continue?": "Mindenki számára befejeződik a konferencia. Folytatod?",
|
||||
"Offline encrypted messaging using dehydrated devices": "Kapcsolat nélküli titkosított üzenetküldés tartósított eszközökkel",
|
||||
"Ignored attempt to disable encryption": "A titkosítás kikapcsolására tett kísérlet figyelmen kívül lett hagyva",
|
||||
"Join the conference at the top of this room": "Csatlakozz a konferenciához a szoba tetején",
|
||||
"Join the conference from the room information card on the right": "Csatlakozz a konferenciához a jobb oldali szoba információs panel segítségével",
|
||||
"Video conference ended by %(senderName)s": "A videókonferenciát befejezte: %(senderName)s",
|
||||
"Video conference updated by %(senderName)s": "A videókonferenciát frissítette: %(senderName)s",
|
||||
"Video conference started by %(senderName)s": "A videókonferenciát elindította: %(senderName)s",
|
||||
"Failed to save your profile": "A profilodat nem sikerült elmenteni",
|
||||
"The operation could not be completed": "A műveletet nem lehetett befejezni",
|
||||
"Remove messages sent by others": "Mások által küldött üzenetek törlése"
|
||||
}
|
||||
|
||||
@@ -2515,5 +2515,13 @@
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Questa versione di %(brand)s non supporta la visualizzazione di alcuni file cifrati",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Questa versione di %(brand)s non supporta la ricerca di messaggi cifrati",
|
||||
"Cannot create rooms in this community": "Impossibile creare stanze in questa comunità",
|
||||
"You do not have permission to create rooms in this community.": "Non hai i permessi per creare stanze in questa comunità."
|
||||
"You do not have permission to create rooms in this community.": "Non hai i permessi per creare stanze in questa comunità.",
|
||||
"Join the conference at the top of this room": "Entra nella conferenza in cima alla stanza",
|
||||
"Join the conference from the room information card on the right": "Entra nella conferenza dalla scheda di informazione della stanza a destra",
|
||||
"Video conference ended by %(senderName)s": "Conferenza video terminata da %(senderName)s",
|
||||
"Video conference updated by %(senderName)s": "Conferenza video aggiornata da %(senderName)s",
|
||||
"Video conference started by %(senderName)s": "Conferenza video iniziata da %(senderName)s",
|
||||
"End conference": "Termina conferenza",
|
||||
"This will end the conference for everyone. Continue?": "Verrà terminata la conferenza per tutti. Continuare?",
|
||||
"Ignored attempt to disable encryption": "Tentativo di disattivare la crittografia ignorato"
|
||||
}
|
||||
|
||||
@@ -1325,7 +1325,7 @@
|
||||
"Invalid homeserver discovery response": "Неверный ответ при попытке обнаружения домашнего сервера",
|
||||
"Failed to get autodiscovery configuration from server": "Не удалось получить конфигурацию автообнаружения с сервера",
|
||||
"Invalid base_url for m.homeserver": "Неверный base_url для m.homeserver",
|
||||
"Homeserver URL does not appear to be a valid Matrix homeserver": "URL-адрес сервера не является действительным URL-адресом сервера Матрица",
|
||||
"Homeserver URL does not appear to be a valid Matrix homeserver": "URL-адрес домашнего сервера не является допустимым домашним сервером Matrix",
|
||||
"Invalid identity server discovery response": "Неверный ответ на запрос идентификации сервера",
|
||||
"Invalid base_url for m.identity_server": "Неверный base_url для m.identity_server",
|
||||
"Identity server URL does not appear to be a valid identity server": "URL-адрес сервера идентификации не является действительным сервером идентификации",
|
||||
@@ -2411,7 +2411,7 @@
|
||||
"Explore public rooms": "Просмотреть публичные комнаты",
|
||||
"Uploading logs": "Загрузка журналов",
|
||||
"Downloading logs": "Скачивание журналов",
|
||||
"Can't see what you’re looking for?": "Не видите то, что ищете?",
|
||||
"Can't see what you’re looking for?": "Не нашли, что искали?",
|
||||
"Explore all public rooms": "Просмотреть все публичные комнаты",
|
||||
"%(count)s results|other": "%(count)s результатов",
|
||||
"Preparing to download logs": "Подготовка к загрузке журналов",
|
||||
@@ -2508,5 +2508,14 @@
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Эта версия %(brand)s не поддерживает просмотр некоторых зашифрованных файлов",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Эта версия %(brand)s не поддерживает поиск зашифрованных сообщений",
|
||||
"Cannot create rooms in this community": "Невозможно создать комнаты в этом сообществе",
|
||||
"You do not have permission to create rooms in this community.": "У вас нет разрешения на создание комнат в этом сообществе."
|
||||
"You do not have permission to create rooms in this community.": "У вас нет разрешения на создание комнат в этом сообществе.",
|
||||
"Join the conference at the top of this room": "Присоединяйтесь к конференции в верхней части этой комнаты",
|
||||
"Join the conference from the room information card on the right": "Присоединяйтесь к конференции, используя информационную карточку комнаты справа",
|
||||
"Video conference ended by %(senderName)s": "%(senderName)s завершил(а) видеоконференцию",
|
||||
"Video conference updated by %(senderName)s": "%(senderName)s обновил(а) видеоконференцию",
|
||||
"Video conference started by %(senderName)s": "%(senderName)s начал(а) видеоконференцию",
|
||||
"End conference": "Завершить конференцию",
|
||||
"This will end the conference for everyone. Continue?": "Это завершит конференцию для всех. Продолжить?",
|
||||
"Failed to save your profile": "Не удалось сохранить ваш профиль",
|
||||
"The operation could not be completed": "Операция не может быть выполнена"
|
||||
}
|
||||
|
||||
@@ -11,5 +11,14 @@
|
||||
"Custom Server Options": "Možnosti strežnika po meri",
|
||||
"Your language of choice": "Vaš jezik po izbiri",
|
||||
"Use Single Sign On to continue": "Uporabi Single Sign On za prijavo",
|
||||
"Confirm adding this email address by using Single Sign On to prove your identity.": ""
|
||||
"Confirm adding this email address by using Single Sign On to prove your identity.": "Potrdite dodajanje tega e-poštnega naslova z enkratno prijavo, da dokažete svojo identiteto.",
|
||||
"Single Sign On": "Enkratna prijava",
|
||||
"Confirm adding email": "Potrdi dodajanje e-poštnega naslova",
|
||||
"Click the button below to confirm adding this email address.": "Kliknite gumb spodaj za potrditev dodajanja tega elektronskega naslova.",
|
||||
"Confirm": "Potrdi",
|
||||
"Add Email Address": "Dodaj e-poštni naslov",
|
||||
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Potrdite dodajanje te telefonske številke z enkratno prijavo, da dokažete svojo identiteto.",
|
||||
"Confirm adding phone number": "Potrdi dodajanje telefonske številke",
|
||||
"Click the button below to confirm adding this phone number.": "Pritisnite gumb spodaj da potrdite dodajanje te telefonske številke.",
|
||||
"Add Phone Number": "Dodaj telefonsko številko"
|
||||
}
|
||||
|
||||
@@ -2500,5 +2500,23 @@
|
||||
"Start a conversation with someone using their name or username (like <userId/>).": "Nisni një bisedë me dikë duke përdorur emrin e tij ose emrin e tij të përdoruesit (bie fjala, <userId/>).",
|
||||
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Kjo s’do ta ftojë te %(communityName)s. Që të ftoni dikë te %(communityName)s, klikoni <a>këtu</a>",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Ftoni dikë duke përdorur emrin e tij, emrin e tij të përdoruesit (bie fjala, <userId/>) ose <a>ndani me të këtë dhomë</a>.",
|
||||
"Unable to set up keys": "S’arrihet të ujdisen kyçe"
|
||||
"Unable to set up keys": "S’arrihet të ujdisen kyçe",
|
||||
"End conference": "Përfundoje konferencën",
|
||||
"This will end the conference for everyone. Continue?": "Kjo do të mbyllë konferencën për këdo. Të vazhdohet?",
|
||||
"Offline encrypted messaging using dehydrated devices": "Shkëmbim jashtë linje mesazhesh të fshehtëzuar duke përdorur pajisje të dehidratuara",
|
||||
"Cross-signing is ready for use.": "“Cross-signing” është gati për përdorim.",
|
||||
"Cross-signing is not set up.": "“Cross-signing” s’është ujdisur.",
|
||||
"Remove messages sent by others": "Hiqi mesazhet e dërguar nga të tjerët",
|
||||
"Ignored attempt to disable encryption": "U shpërfill përpjekje për të çaktivizuar fshehtëzimin",
|
||||
"Join the conference at the top of this room": "Merrni pjesë në konferencë, në krye të kësaj dhome",
|
||||
"Join the conference from the room information card on the right": "Merrni pjesë në konferencë që prej kartës së informacionit mbi dhomën, në të djathtë",
|
||||
"Video conference ended by %(senderName)s": "Konferenca video u përfundua nga %(senderName)s",
|
||||
"Video conference updated by %(senderName)s": "Konferenca video u përditësua nga %(senderName)s",
|
||||
"Video conference started by %(senderName)s": "Konferenca video u fillua nga %(senderName)s",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Që të shihni krejt kartelat e fshehtëzuara, përdorni <a>aplikacionin për Desktop</a>",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "Që të kërkoni te mesazhe të fshehtëzuar, përdorni <a>aplikacionin për Desktop</a>",
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Ky version i %(brand)s nuk mbulon parjen për disa kartela të fshehtëzuara",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Ky version i %(brand)s nuk mbulon kërkimin në mesazhe të fshehtëzuar",
|
||||
"Cannot create rooms in this community": "S’mund të krijohen dhoma në këtë bashkësi",
|
||||
"You do not have permission to create rooms in this community.": "S’keni leje të krijoni dhoma në këtë bashkësi."
|
||||
}
|
||||
|
||||
@@ -2439,5 +2439,23 @@
|
||||
"Start a conversation with someone using their name or username (like <userId/>).": "Starta en konversation med någon med deras namn eller användarnamn (som <userId/>).",
|
||||
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Detta kommer inte att bjuda in dem till %(communityName)s. För att bjuda in någon till %(communityName)s, klicka <a>här</a>",
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Bjud in någon med deras namn eller användarnamn (som <userId/>) eller <a>dela det här rummet</a>.",
|
||||
"Unable to set up keys": "Kunde inte ställa in nycklar"
|
||||
"Unable to set up keys": "Kunde inte ställa in nycklar",
|
||||
"End conference": "Avsluta gruppsamtal",
|
||||
"This will end the conference for everyone. Continue?": "Detta kommer att avsluta gruppsamtalet för alla. Fortsätt?",
|
||||
"Offline encrypted messaging using dehydrated devices": "Krypterad meddelandehantering offline med hjälp av frystorkade enheter",
|
||||
"Failed to save your profile": "Misslyckades att spara din profil",
|
||||
"The operation could not be completed": "Operationen kunde inte slutföras",
|
||||
"Remove messages sent by others": "Ta bort meddelanden skickade av andra",
|
||||
"Ignored attempt to disable encryption": "Ignorerade försök att inaktivera kryptering",
|
||||
"Join the conference at the top of this room": "Gå med i gruppsamtalet på toppen av det här rummet",
|
||||
"Join the conference from the room information card on the right": "Gå med i gruppsamtalet ifrån informationskortet till höger",
|
||||
"Video conference ended by %(senderName)s": "Videogruppsamtal avslutat av %(senderName)s",
|
||||
"Video conference updated by %(senderName)s": "Videogruppsamtal uppdaterat av %(senderName)s",
|
||||
"Video conference started by %(senderName)s": "Videogruppsamtal startat av %(senderName)s",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Använd <a>skrivbordsappen</a> för att se alla krypterade filer",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "Använd <a>skrivbordsappen</a> söka bland krypterade meddelanden",
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "Den här versionen av %(brand)s stöder inte visning av vissa krypterade filer",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "Den här versionen av %(brand)s stöder inte sökning bland krypterade meddelanden",
|
||||
"Cannot create rooms in this community": "Kan inte skapa rum i den här gemenskapen",
|
||||
"You do not have permission to create rooms in this community.": "Du har inte behörighet att skapa rum i den här gemenskapen."
|
||||
}
|
||||
|
||||
@@ -642,7 +642,7 @@
|
||||
"Sunday": "星期日",
|
||||
"Notification targets": "通知目标",
|
||||
"Today": "今天",
|
||||
"You are not receiving desktop notifications": "您将不会收到桌面通知",
|
||||
"You are not receiving desktop notifications": "您现在不会收到桌面通知",
|
||||
"Friday": "星期五",
|
||||
"Update": "更新",
|
||||
"What's New": "更新内容",
|
||||
@@ -1278,7 +1278,7 @@
|
||||
"If you cancel now, you won't complete verifying your other session.": "如果现在取消,您将无法完成验证您的其他会话。",
|
||||
"If you cancel now, you won't complete your operation.": "如果现在取消,您将无法完成您的操作。",
|
||||
"Cancel entering passphrase?": "取消输入密码?",
|
||||
"Setting up keys": "设置按键",
|
||||
"Setting up keys": "设置密钥",
|
||||
"Verify this session": "验证此会话",
|
||||
"Encryption upgrade available": "提供加密升级",
|
||||
"Set up encryption": "设置加密",
|
||||
@@ -2384,5 +2384,19 @@
|
||||
"Cross-signing is ready for use.": "交叉签名已可用。",
|
||||
"Cross-signing is not set up.": "未设置交叉签名。",
|
||||
"Backup version:": "备份版本:",
|
||||
"Algorithm:": "算法:"
|
||||
"Algorithm:": "算法:",
|
||||
"Set up Secure Backup": "设置安全备份",
|
||||
"Safeguard against losing access to encrypted messages & data": "保护加密信息 & 数据的访问权",
|
||||
"not found in storage": "未在存储中找到",
|
||||
"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.": "请备份加密密钥及帐户数据,以防无法访问您的会话。您的密钥将使用唯一的恢复密钥进行保护。",
|
||||
"Backup key stored:": "备份密钥已保存:",
|
||||
"Backup key cached:": "备份密钥已缓存:",
|
||||
"Secret storage:": "秘密存储:",
|
||||
"ready": "就绪",
|
||||
"not ready": "尚未就绪",
|
||||
"Secure Backup": "安全备份",
|
||||
"Privacy": "隐私",
|
||||
"Explore community rooms": "探索社区聊天室",
|
||||
"%(count)s results|one": "%(count)s 个结果",
|
||||
"Room Info": "聊天室信息"
|
||||
}
|
||||
|
||||
@@ -2515,5 +2515,17 @@
|
||||
"This version of %(brand)s does not support viewing some encrypted files": "此版本的 %(brand)s 不支援檢視某些加密檔案",
|
||||
"This version of %(brand)s does not support searching encrypted messages": "此版本的 %(brand)s 不支援搜尋加密訊息",
|
||||
"Cannot create rooms in this community": "無法在此社群中建立聊天室",
|
||||
"You do not have permission to create rooms in this community.": "您沒有在此社群中建立聊天室的權限。"
|
||||
"You do not have permission to create rooms in this community.": "您沒有在此社群中建立聊天室的權限。",
|
||||
"Join the conference at the top of this room": "加入此聊天室頂部的會議",
|
||||
"Join the conference from the room information card on the right": "從右側的聊天室資訊卡片加入會議",
|
||||
"Video conference ended by %(senderName)s": "視訊會議由 %(senderName)s 結束",
|
||||
"Video conference updated by %(senderName)s": "視訊會議由 %(senderName)s 更新",
|
||||
"Video conference started by %(senderName)s": "視訊會議由 %(senderName)s 開始",
|
||||
"End conference": "結束會議",
|
||||
"This will end the conference for everyone. Continue?": "這將會對所有人結束會議。要繼續嗎?",
|
||||
"Ignored attempt to disable encryption": "已忽略嘗試停用加密",
|
||||
"Offline encrypted messaging using dehydrated devices": "使用乾淨裝置的離線加密訊息",
|
||||
"Failed to save your profile": "儲存您的設定檔失敗",
|
||||
"The operation could not be completed": "無法完成操作",
|
||||
"Remove messages sent by others": "移除其他人傳送的訊息"
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations 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 dis from '../dispatcher/dispatcher';
|
||||
import {Store} from 'flux/utils';
|
||||
|
||||
const INITIAL_STATE = {
|
||||
cachedPassword: localStorage.getItem('mx_pass'),
|
||||
};
|
||||
|
||||
/**
|
||||
* A class for storing application state to do with the session. This is a simple flux
|
||||
* store that listens for actions and updates its state accordingly, informing any
|
||||
* listeners (views) of state changes.
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* sessionStore.addListener(() => {
|
||||
* this.setState({ cachedPassword: sessionStore.getCachedPassword() })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
class SessionStore extends Store {
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
||||
// Initialise state
|
||||
this._state = INITIAL_STATE;
|
||||
}
|
||||
|
||||
_update() {
|
||||
// Persist state to localStorage
|
||||
if (this._state.cachedPassword) {
|
||||
localStorage.setItem('mx_pass', this._state.cachedPassword);
|
||||
} else {
|
||||
localStorage.removeItem('mx_pass', this._state.cachedPassword);
|
||||
}
|
||||
|
||||
this.__emitChange();
|
||||
}
|
||||
|
||||
_setState(newState) {
|
||||
this._state = Object.assign(this._state, newState);
|
||||
this._update();
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
switch (payload.action) {
|
||||
case 'cached_password':
|
||||
this._setState({
|
||||
cachedPassword: payload.cachedPassword,
|
||||
});
|
||||
break;
|
||||
case 'password_changed':
|
||||
this._setState({
|
||||
cachedPassword: null,
|
||||
});
|
||||
break;
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out':
|
||||
this._setState({
|
||||
cachedPassword: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getCachedPassword() {
|
||||
return this._state.cachedPassword;
|
||||
}
|
||||
}
|
||||
|
||||
let singletonSessionStore = null;
|
||||
if (!singletonSessionStore) {
|
||||
singletonSessionStore = new SessionStore();
|
||||
}
|
||||
export default singletonSessionStore;
|
||||
@@ -66,7 +66,7 @@ class ElementWidget extends Widget {
|
||||
if (WidgetType.JITSI.matches(this.type)) {
|
||||
return WidgetUtils.getLocalJitsiWrapperUrl({
|
||||
forLocalRender: true,
|
||||
auth: this.rawData?.auth,
|
||||
auth: super.rawData?.auth, // this.rawData can call templateUrl, do this to prevent looping
|
||||
});
|
||||
}
|
||||
return super.templateUrl;
|
||||
|
||||
@@ -1,47 +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 { _t } from "../languageHandler";
|
||||
import Modal from "../Modal";
|
||||
import SetPasswordDialog from "../components/views/dialogs/SetPasswordDialog";
|
||||
import GenericToast from "../components/views/toasts/GenericToast";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
|
||||
const onAccept = () => {
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
|
||||
};
|
||||
|
||||
const TOAST_KEY = "setpassword";
|
||||
|
||||
export const showToast = () => {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: TOAST_KEY,
|
||||
title: _t("Set password"),
|
||||
props: {
|
||||
description: _t("To return to your account in future you need to set a password"),
|
||||
acceptLabel: _t("Set Password"),
|
||||
onAccept,
|
||||
rejectLabel: _t("Later"),
|
||||
onReject: hideToast, // it'll return on reload
|
||||
},
|
||||
component: GenericToast,
|
||||
priority: 60,
|
||||
});
|
||||
};
|
||||
|
||||
export const hideToast = () => {
|
||||
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
|
||||
};
|
||||
Reference in New Issue
Block a user