Merge branch 'develop' into joriks/appearance-advanced

This commit is contained in:
Jorik Schellekens 2020-06-22 11:27:48 +01:00 committed by GitHub
commit 2294d23b32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 2203 additions and 710 deletions

View file

@ -98,7 +98,3 @@ limitations under the License.
}
}
}
.mx_CompleteSecurity_resetText {
padding-top: 20px;
}

View file

@ -73,42 +73,33 @@ limitations under the License.
margin-left: 20px;
}
.mx_CreateSecretStorageDialog_recoveryKeyHeader {
margin-bottom: 1em;
}
.mx_CreateSecretStorageDialog_recoveryKeyContainer {
width: 380px;
margin-left: auto;
margin-right: auto;
display: flex;
}
.mx_CreateSecretStorageDialog_recoveryKey {
font-weight: bold;
text-align: center;
width: 262px;
padding: 20px;
color: $info-plinth-fg-color;
background-color: $info-plinth-bg-color;
border-radius: 6px;
word-spacing: 1em;
margin-bottom: 20px;
margin-right: 12px;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton {
width: 160px;
padding-left: 0px;
padding-right: 0px;
margin-right: 10px;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
flex: 1;
white-space: nowrap;
}
.mx_CreateSecretStorageDialog_continueSpinner {
margin-top: 33px;
text-align: right;
}
.mx_CreateSecretStorageDialog_continueSpinner img {
width: 20px;
height: 20px;
}

View file

@ -15,5 +15,5 @@ limitations under the License.
*/
// Based on https://stackoverflow.com/a/53229857/3532235
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never}
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never};
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;

View file

@ -150,7 +150,7 @@ export default abstract class BasePlatform {
abstract displayNotification(title: string, msg: string, avatarUrl: string, room: Object);
loudNotification(ev: Event, room: Object) {
};
}
/**
* Returns a promise that resolves to a string representing the current version of the application.

View file

@ -29,8 +29,6 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
// operation ends.
let secretStorageKeys = {};
let secretStorageBeingAccessed = false;
// Stores the 'passphraseOnly' option for the active storage access operation
let passphraseOnlyOption = null;
function isCachingAllowed() {
return secretStorageBeingAccessed;
@ -97,7 +95,6 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
const key = await inputToKey(input);
return await MatrixClientPeg.get().checkSecretStorageKey(key, info);
},
passphraseOnly: passphraseOnlyOption,
},
/* className= */ null,
/* isPriorityModal= */ false,
@ -212,27 +209,19 @@ export async function promptForBackupPassphrase() {
*
* @param {Function} [func] An operation to perform once secret storage has been
* bootstrapped. Optional.
* @param {object} [opts] Named options
* @param {bool} [opts.forceReset] Reset secret storage even if it's already set up
* @param {object} [opts.withKeys] Map of key ID to key for SSSS keys that the client
* already has available. If a key is not supplied here, the user will be prompted.
* @param {bool} [opts.passphraseOnly] If true, do not prompt for recovery key or to reset keys
* @param {bool} [forceReset] Reset secret storage even if it's already set up
*/
export async function accessSecretStorage(
func = async () => { }, opts = {},
) {
export async function accessSecretStorage(func = async () => { }, forceReset = false) {
const cli = MatrixClientPeg.get();
secretStorageBeingAccessed = true;
passphraseOnlyOption = opts.passphraseOnly;
secretStorageKeys = Object.assign({}, opts.withKeys || {});
try {
if (!await cli.hasSecretStorageKey() || opts.forceReset) {
if (!await cli.hasSecretStorageKey() || forceReset) {
// This dialog calls bootstrap itself after guiding the user through
// passphrase creation.
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
{
force: opts.forceReset,
force: forceReset,
},
null, /* priority = */ false, /* static = */ true,
);
@ -270,6 +259,5 @@ export async function accessSecretStorage(
if (!isCachingAllowed()) {
secretStorageKeys = {};
}
passphraseOnlyOption = null;
}
}

View file

@ -119,26 +119,26 @@ export default class DeviceListener {
// No need to do a recheck here: we just need to get a snapshot of our devices
// before we download any new ones.
}
};
_onDevicesUpdated = (users: string[]) => {
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
this._recheck();
}
};
_onDeviceVerificationChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck();
}
};
_onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck();
}
};
_onCrossSingingKeysChanged = () => {
this._recheck();
}
};
_onAccountData = (ev) => {
// User may have:
@ -152,11 +152,11 @@ export default class DeviceListener {
) {
this._recheck();
}
}
};
_onSync = (state, prevState) => {
if (state === 'PREPARED' && prevState === null) this._recheck();
}
};
// The server doesn't tell us when key backup is set up, so we poll
// & cache the result

View file

@ -35,13 +35,13 @@ import { crossSigningCallbacks } from './CrossSigningManager';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
export interface IMatrixClientCreds {
homeserverUrl: string,
identityServerUrl: string,
userId: string,
deviceId: string,
accessToken: string,
guest: boolean,
pickleKey?: string,
homeserverUrl: string;
identityServerUrl: string;
userId: string;
deviceId: string;
accessToken: string;
guest: boolean;
pickleKey?: string;
}
// TODO: Move this to the js-sdk

View file

@ -17,25 +17,71 @@ limitations under the License.
import EventIndexPeg from "./indexing/EventIndexPeg";
import {MatrixClientPeg} from "./MatrixClientPeg";
function serverSideSearch(term, roomId = undefined) {
let filter;
if (roomId !== undefined) {
// XXX: it's unintuitive that the filter for searching doesn't have
// the same shape as the v2 filter API :(
filter = {
rooms: [roomId],
};
}
const SEARCH_LIMIT = 10;
const searchPromise = MatrixClientPeg.get().searchRoomEvents({
filter,
term,
});
async function serverSideSearch(term, roomId = undefined) {
const client = MatrixClientPeg.get();
return searchPromise;
const filter = {
limit: SEARCH_LIMIT,
};
if (roomId !== undefined) filter.rooms = [roomId];
const body = {
search_categories: {
room_events: {
search_term: term,
filter: filter,
order_by: "recent",
event_context: {
before_limit: 1,
after_limit: 1,
include_profile: true,
},
},
},
};
const response = await client.search({body: body});
const result = {
response: response,
query: body,
};
return result;
}
async function serverSideSearchProcess(term, roomId = undefined) {
const client = MatrixClientPeg.get();
const result = await serverSideSearch(term, roomId);
// The js-sdk method backPaginateRoomEventsSearch() uses _query internally
// so we're reusing the concept here since we wan't to delegate the
// pagination back to backPaginateRoomEventsSearch() in some cases.
const searchResult = {
_query: result.query,
results: [],
highlights: [],
};
return client._processRoomEventsSearch(searchResult, result.response);
}
function compareEvents(a, b) {
const aEvent = a.result;
const bEvent = b.result;
if (aEvent.origin_server_ts > bEvent.origin_server_ts) return -1;
if (aEvent.origin_server_ts < bEvent.origin_server_ts) return 1;
return 0;
}
async function combinedSearch(searchTerm) {
const client = MatrixClientPeg.get();
// Create two promises, one for the local search, one for the
// server-side search.
const serverSidePromise = serverSideSearch(searchTerm);
@ -48,37 +94,59 @@ async function combinedSearch(searchTerm) {
const localResult = await localPromise;
const serverSideResult = await serverSidePromise;
// Combine the search results into one result.
const result = {};
const serverQuery = serverSideResult.query;
const serverResponse = serverSideResult.response;
// Our localResult and serverSideResult are both ordered by
// recency separately, when we combine them the order might not
// be the right one so we need to sort them.
const compare = (a, b) => {
const aEvent = a.context.getEvent().event;
const bEvent = b.context.getEvent().event;
const localQuery = localResult.query;
const localResponse = localResult.response;
if (aEvent.origin_server_ts >
bEvent.origin_server_ts) return -1;
if (aEvent.origin_server_ts <
bEvent.origin_server_ts) return 1;
return 0;
// Store our queries for later on so we can support pagination.
//
// We're reusing _query here again to not introduce separate code paths and
// concepts for our different pagination methods. We're storing the
// server-side next batch separately since the query is the json body of
// the request and next_batch needs to be a query parameter.
//
// We can't put it in the final result that _processRoomEventsSearch()
// returns since that one can be either a server-side one, a local one or a
// fake one to fetch the remaining cached events. See the docs for
// combineEvents() for an explanation why we need to cache events.
const emptyResult = {
seshatQuery: localQuery,
_query: serverQuery,
serverSideNextBatch: serverResponse.next_batch,
cachedEvents: [],
oldestEventFrom: "server",
results: [],
highlights: [],
};
result.count = localResult.count + serverSideResult.count;
result.results = localResult.results.concat(
serverSideResult.results).sort(compare);
result.highlights = localResult.highlights.concat(
serverSideResult.highlights);
// Combine our results.
const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events);
// Let the client process the combined result.
const response = {
search_categories: {
room_events: combinedResult,
},
};
const result = client._processRoomEventsSearch(emptyResult, response);
// Restore our encryption info so we can properly re-verify the events.
restoreEncryptionInfo(result.results);
return result;
}
async function localSearch(searchTerm, roomId = undefined) {
async function localSearch(searchTerm, roomId = undefined, processResult = true) {
const eventIndex = EventIndexPeg.get();
const searchArgs = {
search_term: searchTerm,
before_limit: 1,
after_limit: 1,
limit: SEARCH_LIMIT,
order_by_recency: true,
room_id: undefined,
};
@ -87,6 +155,19 @@ async function localSearch(searchTerm, roomId = undefined) {
searchArgs.room_id = roomId;
}
const localResult = await eventIndex.search(searchArgs);
searchArgs.next_batch = localResult.next_batch;
const result = {
response: localResult,
query: searchArgs,
};
return result;
}
async function localSearchProcess(searchTerm, roomId = undefined) {
const emptyResult = {
results: [],
highlights: [],
@ -94,9 +175,34 @@ async function localSearch(searchTerm, roomId = undefined) {
if (searchTerm === "") return emptyResult;
const result = await localSearch(searchTerm, roomId);
emptyResult.seshatQuery = result.query;
const response = {
search_categories: {
room_events: result.response,
},
};
const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response);
// Restore our encryption info so we can properly re-verify the events.
restoreEncryptionInfo(processedResult.results);
return processedResult;
}
async function localPagination(searchResult) {
const eventIndex = EventIndexPeg.get();
const searchArgs = searchResult.seshatQuery;
const localResult = await eventIndex.search(searchArgs);
searchResult.seshatQuery.next_batch = localResult.next_batch;
// We only need to restore the encryption state for the new results, so
// remember how many of them we got.
const newResultCount = localResult.results.length;
const response = {
search_categories: {
@ -104,15 +210,257 @@ async function localSearch(searchTerm, roomId = undefined) {
},
};
const result = MatrixClientPeg.get()._processRoomEventsSearch(
emptyResult, response);
const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response);
// Restore our encryption info so we can properly re-verify the events.
for (let i = 0; i < result.results.length; i++) {
const timeline = result.results[i].context.getTimeline();
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
restoreEncryptionInfo(newSlice);
searchResult.pendingRequest = null;
return result;
}
function compareOldestEvents(firstResults, secondResults) {
try {
const oldestFirstEvent = firstResults.results[firstResults.results.length - 1].result;
const oldestSecondEvent = secondResults.results[secondResults.results.length - 1].result;
if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) {
return -1;
} else {
return 1;
}
} catch {
return 0;
}
}
function combineEventSources(previousSearchResult, response, a, b) {
// Merge event sources and sort the events.
const combinedEvents = a.concat(b).sort(compareEvents);
// Put half of the events in the response, and cache the other half.
response.results = combinedEvents.slice(0, SEARCH_LIMIT);
previousSearchResult.cachedEvents = combinedEvents.slice(SEARCH_LIMIT);
}
/**
* Combine the events from our event sources into a sorted result
*
* This method will first be called from the combinedSearch() method. In this
* case we will fetch SEARCH_LIMIT events from the server and the local index.
*
* The method will put the SEARCH_LIMIT newest events from the server and the
* local index in the results part of the response, the rest will be put in the
* cachedEvents field of the previousSearchResult (in this case an empty search
* result).
*
* Every subsequent call will be made from the combinedPagination() method, in
* this case we will combine the cachedEvents and the next SEARCH_LIMIT events
* from either the server or the local index.
*
* Since we have two event sources and we need to sort the results by date we
* need keep on looking for the oldest event. We are implementing a variation of
* a sliding window.
*
* The event sources are here represented as two sorted lists where the smallest
* number represents the newest event. The two lists need to be merged in a way
* that preserves the sorted property so they can be shown as one search result.
* We first fetch SEARCH_LIMIT events from both sources.
*
* If we set SEARCH_LIMIT to 3:
*
* Server events [01, 02, 04, 06, 07, 08, 11, 13]
* |01, 02, 04|
* Local events [03, 05, 09, 10, 12, 14, 15, 16]
* |03, 05, 09|
*
* We note that the oldest event is from the local index, and we combine the
* results:
*
* Server window [01, 02, 04]
* Local window [03, 05, 09]
*
* Combined events [01, 02, 03, 04, 05, 09]
*
* We split the combined result in the part that we want to present and a part
* that will be cached.
*
* Presented events [01, 02, 03]
* Cached events [04, 05, 09]
*
* We slide the window for the server since the oldest event is from the local
* index.
*
* Server events [01, 02, 04, 06, 07, 08, 11, 13]
* |06, 07, 08|
* Local events [03, 05, 09, 10, 12, 14, 15, 16]
* |XX, XX, XX|
* Cached events [04, 05, 09]
*
* We note that the oldest event is from the server and we combine the new
* server events with the cached ones.
*
* Cached events [04, 05, 09]
* Server events [06, 07, 08]
*
* Combined events [04, 05, 06, 07, 08, 09]
*
* We split again.
*
* Presented events [04, 05, 06]
* Cached events [07, 08, 09]
*
* We slide the local window, the oldest event is on the server.
*
* Server events [01, 02, 04, 06, 07, 08, 11, 13]
* |XX, XX, XX|
* Local events [03, 05, 09, 10, 12, 14, 15, 16]
* |10, 12, 14|
*
* Cached events [07, 08, 09]
* Local events [10, 12, 14]
* Combined events [07, 08, 09, 10, 12, 14]
*
* Presented events [07, 08, 09]
* Cached events [10, 12, 14]
*
* Next up we slide the server window again.
*
* Server events [01, 02, 04, 06, 07, 08, 11, 13]
* |11, 13|
* Local events [03, 05, 09, 10, 12, 14, 15, 16]
* |XX, XX, XX|
*
* Cached events [10, 12, 14]
* Server events [11, 13]
* Combined events [10, 11, 12, 13, 14]
*
* Presented events [10, 11, 12]
* Cached events [13, 14]
*
* We have one source exhausted, we fetch the rest of our events from the other
* source and combine it with our cached events.
*
*
* @param {object} previousSearchResult A search result from a previous search
* call.
* @param {object} localEvents An unprocessed search result from the event
* index.
* @param {object} serverEvents An unprocessed search result from the server.
*
* @return {object} A response object that combines the events from the
* different event sources.
*
*/
function combineEvents(previousSearchResult, localEvents = undefined, serverEvents = undefined) {
const response = {};
const cachedEvents = previousSearchResult.cachedEvents;
let oldestEventFrom = previousSearchResult.oldestEventFrom;
response.highlights = previousSearchResult.highlights;
if (localEvents && serverEvents) {
// This is a first search call, combine the events from the server and
// the local index. Note where our oldest event came from, we shall
// fetch the next batch of events from the other source.
if (compareOldestEvents(localEvents, serverEvents) < 0) {
oldestEventFrom = "local";
}
combineEventSources(previousSearchResult, response, localEvents.results, serverEvents.results);
response.highlights = localEvents.highlights.concat(serverEvents.highlights);
} else if (localEvents) {
// This is a pagination call fetching more events from the local index,
// meaning that our oldest event was on the server.
// Change the source of the oldest event if our local event is older
// than the cached one.
if (compareOldestEvents(localEvents, cachedEvents) < 0) {
oldestEventFrom = "local";
}
combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents);
} else if (serverEvents) {
// This is a pagination call fetching more events from the server,
// meaning that our oldest event was in the local index.
// Change the source of the oldest event if our server event is older
// than the cached one.
if (compareOldestEvents(serverEvents, cachedEvents) < 0) {
oldestEventFrom = "server";
}
combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents);
} else {
// This is a pagination call where we exhausted both of our event
// sources, let's push the remaining cached events.
response.results = cachedEvents;
previousSearchResult.cachedEvents = [];
}
previousSearchResult.oldestEventFrom = oldestEventFrom;
return response;
}
/**
* Combine the local and server search responses
*
* @param {object} previousSearchResult A search result from a previous search
* call.
* @param {object} localEvents An unprocessed search result from the event
* index.
* @param {object} serverEvents An unprocessed search result from the server.
*
* @return {object} A response object that combines the events from the
* different event sources.
*/
function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) {
// Combine our events first.
const response = combineEvents(previousSearchResult, localEvents, serverEvents);
// Our first search will contain counts from both sources, subsequent
// pagination requests will fetch responses only from one of the sources, so
// reuse the first count when we're paginating.
if (previousSearchResult.count) {
response.count = previousSearchResult.count;
} else {
response.count = localEvents.count + serverEvents.count;
}
// Update our next batch tokens for the given search sources.
if (localEvents) {
previousSearchResult.seshatQuery.next_batch = localEvents.next_batch;
}
if (serverEvents) {
previousSearchResult.serverSideNextBatch = serverEvents.next_batch;
}
// Set the response next batch token to one of the tokens from the sources,
// this makes sure that if we exhaust one of the sources we continue with
// the other one.
if (previousSearchResult.seshatQuery.next_batch) {
response.next_batch = previousSearchResult.seshatQuery.next_batch;
} else if (previousSearchResult.serverSideNextBatch) {
response.next_batch = previousSearchResult.serverSideNextBatch;
}
// We collected all search results from the server as well as from Seshat,
// we still have some events cached that we'll want to display on the next
// pagination request.
//
// Provide a fake next batch token for that case.
if (!response.next_batch && previousSearchResult.cachedEvents.length > 0) {
response.next_batch = "cached";
}
return response;
}
function restoreEncryptionInfo(searchResultSlice) {
for (let i = 0; i < searchResultSlice.length; i++) {
const timeline = searchResultSlice[i].context.getTimeline();
for (let j = 0; j < timeline.length; j++) {
const ev = timeline[j];
if (ev.event.curve25519Key) {
ev.makeEncrypted(
"m.room.encrypted",
@ -129,6 +477,57 @@ async function localSearch(searchTerm, roomId = undefined) {
}
}
}
}
async function combinedPagination(searchResult) {
const eventIndex = EventIndexPeg.get();
const client = MatrixClientPeg.get();
const searchArgs = searchResult.seshatQuery;
const oldestEventFrom = searchResult.oldestEventFrom;
let localResult;
let serverSideResult;
// Fetch events from the local index if we have a token for itand if it's
// the local indexes turn or the server has exhausted its results.
if (searchArgs.next_batch && (!searchResult.serverSideNextBatch || oldestEventFrom === "server")) {
localResult = await eventIndex.search(searchArgs);
}
// Fetch events from the server if we have a token for it and if it's the
// local indexes turn or the local index has exhausted its results.
if (searchResult.serverSideNextBatch && (oldestEventFrom === "local" || !searchArgs.next_batch)) {
const body = {body: searchResult._query, next_batch: searchResult.serverSideNextBatch};
serverSideResult = await client.search(body);
}
let serverEvents;
if (serverSideResult) {
serverEvents = serverSideResult.search_categories.room_events;
}
// Combine our events.
const combinedResult = combineResponses(searchResult, localResult, serverEvents);
const response = {
search_categories: {
room_events: combinedResult,
},
};
const oldResultCount = searchResult.results.length;
// Let the client process the combined result.
const result = client._processRoomEventsSearch(searchResult, response);
// Restore our encryption info so we can properly re-verify the events.
const newResultCount = result.results.length - oldResultCount;
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
restoreEncryptionInfo(newSlice);
searchResult.pendingRequest = null;
return result;
}
@ -140,11 +539,11 @@ function eventIndexSearch(term, roomId = undefined) {
if (MatrixClientPeg.get().isRoomEncrypted(roomId)) {
// The search is for a single encrypted room, use our local
// search method.
searchPromise = localSearch(term, roomId);
searchPromise = localSearchProcess(term, roomId);
} else {
// The search is for a single non-encrypted room, use the
// server-side search.
searchPromise = serverSideSearch(term, roomId);
searchPromise = serverSideSearchProcess(term, roomId);
}
} else {
// Search across all rooms, combine a server side search and a
@ -155,9 +554,45 @@ function eventIndexSearch(term, roomId = undefined) {
return searchPromise;
}
function eventIndexSearchPagination(searchResult) {
const client = MatrixClientPeg.get();
const seshatQuery = searchResult.seshatQuery;
const serverQuery = searchResult._query;
if (!seshatQuery) {
// This is a search in a non-encrypted room. Do the normal server-side
// pagination.
return client.backPaginateRoomEventsSearch(searchResult);
} else if (!serverQuery) {
// This is a search in a encrypted room. Do a local pagination.
const promise = localPagination(searchResult);
searchResult.pendingRequest = promise;
return promise;
} else {
// We have both queries around, this is a search across all rooms so a
// combined pagination needs to be done.
const promise = combinedPagination(searchResult);
searchResult.pendingRequest = promise;
return promise;
}
}
export function searchPagination(searchResult) {
const eventIndex = EventIndexPeg.get();
const client = MatrixClientPeg.get();
if (searchResult.pendingRequest) return searchResult.pendingRequest;
if (eventIndex === null) return client.backPaginateRoomEventsSearch(searchResult);
else return eventIndexSearchPagination(searchResult);
}
export default function eventSearch(term, roomId = undefined) {
const eventIndex = EventIndexPeg.get();
if (eventIndex === null) return serverSideSearch(term, roomId);
if (eventIndex === null) return serverSideSearchProcess(term, roomId);
else return eventIndexSearch(term, roomId);
}

View file

@ -60,7 +60,7 @@ export default class TagOrderActions {
// For an optimistic update
return {tags, removedTags};
});
};
}
/**
* Creates an action thunk that will do an asynchronous request to

View file

@ -20,23 +20,25 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import FileSaver from 'file-saver';
import {_t} from '../../../../languageHandler';
import {_t, _td} from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
import {copyNode} from "../../../../utils/strings";
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
const PHASE_MIGRATE = 2;
const PHASE_INTRO = 3;
const PHASE_SHOWKEY = 4;
const PHASE_STORING = 5;
const PHASE_CONFIRM_SKIP = 6;
const PHASE_PASSPHRASE = 3;
const PHASE_PASSPHRASE_CONFIRM = 4;
const PHASE_SHOWKEY = 5;
const PHASE_KEEPITSAFE = 6;
const PHASE_STORING = 7;
const PHASE_DONE = 8;
const PHASE_CONFIRM_SKIP = 9;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
/*
* Walks the user through the process of creating a passphrase to guard Secure
@ -63,32 +65,34 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.state = {
phase: PHASE_LOADING,
downloaded: false,
passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '',
copied: false,
downloaded: false,
backupInfo: null,
backupInfoFetched: false,
backupInfoFetchError: null,
backupSigStatus: null,
// does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload? (If we have an account password, we
// assume that it can)
// for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: null,
canUploadKeyCheckInProgress: false,
accountPassword: props.accountPassword || "",
accountPasswordCorrect: null,
// No toggle for this: if we really don't want one, remove it & just hard code true
// status of the key backup toggle switch
useKeyBackup: true,
};
if (props.accountPassword) {
// If we have an account password, we assume we can upload keys with
// just a password (otherwise leave it as null so we poll to check)
this.state.canUploadKeysWithPasswordOnly = true;
}
this._passphraseField = createRef();
this.loadData();
this._fetchBackupInfo();
if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
this.state.canUploadKeysWithPasswordOnly = true;
} else {
this._queryKeyUploadAuth();
}
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
}
@ -105,11 +109,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
);
const { force } = this.props;
const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE;
this.setState({
backupInfoFetched: true,
phase,
backupInfo,
backupSigStatus,
backupInfoFetchError: null,
});
return {
@ -117,25 +123,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
backupSigStatus,
};
} catch (e) {
this.setState({backupInfoFetchError: e});
this.setState({phase: PHASE_LOADERROR});
}
}
async _queryKeyUploadAuth() {
try {
this.setState({canUploadKeyCheckInProgress: true});
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
this.setState({canUploadKeyCheckInProgress: false});
} catch (error) {
if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!");
this.setState({
canUploadKeyCheckInProgress: false,
});
return;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
@ -143,18 +144,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
});
this.setState({
canUploadKeysWithPasswordOnly,
canUploadKeyCheckInProgress: false,
});
}
}
async _createRecoveryKey() {
this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({
phase: PHASE_SHOWKEY,
});
}
_onKeyBackupStatusChange = () => {
if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo();
}
@ -163,6 +156,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._recoveryKeyNode = n;
}
_onUseKeyBackupChange = (enabled) => {
this.setState({
useKeyBackup: enabled,
});
}
_onMigrateFormSubmit = (e) => {
e.preventDefault();
if (this.state.backupSigStatus.usable) {
@ -172,15 +171,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
}
_onIntroContinueClick = () => {
this._createRecoveryKey();
}
_onCopyClick = () => {
const successful = copyNode(this._recoveryKeyNode);
if (successful) {
this.setState({
copied: true,
phase: PHASE_KEEPITSAFE,
});
}
}
@ -190,8 +186,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'recovery-key.txt');
this.setState({
downloaded: true,
phase: PHASE_KEEPITSAFE,
});
}
@ -247,9 +245,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_bootstrapSecretStorage = async () => {
this.setState({
// we use LOADING here rather than STORING as STORING still shows the 'show key'
// screen which is not relevant: LOADING is just a generic spinner.
phase: PHASE_LOADING,
phase: PHASE_STORING,
error: null,
});
@ -290,7 +286,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
},
});
}
this.props.onFinished(true);
this.setState({
phase: PHASE_DONE,
});
} catch (e) {
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
this.setState({
@ -309,6 +307,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this.props.onFinished(false);
}
_onDone = () => {
this.props.onFinished(true);
}
_restoreBackup = async () => {
// It's possible we'll need the backup key later on for bootstrapping,
// so let's stash it here, rather than prompting for it twice.
@ -335,41 +337,88 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
}
_onShowKeyContinueClick = () => {
this._bootstrapSecretStorage();
}
_onLoadRetryClick = () => {
this.loadData();
}
async loadData() {
this.setState({phase: PHASE_LOADING});
const proms = [];
if (!this.state.backupInfoFetched) proms.push(this._fetchBackupInfo());
if (this.state.canUploadKeysWithPasswordOnly === null) proms.push(this._queryKeyUploadAuth());
await Promise.all(proms);
if (this.state.canUploadKeysWithPasswordOnly === null || this.state.backupInfoFetchError) {
this.setState({phase: PHASE_LOADERROR});
} else if (this.state.backupInfo && !this.props.force) {
this.setState({phase: PHASE_MIGRATE});
} else {
this.setState({phase: PHASE_INTRO});
}
this._fetchBackupInfo();
}
_onSkipSetupClick = () => {
this.setState({phase: PHASE_CONFIRM_SKIP});
}
_onGoBackClick = () => {
if (this.state.backupInfo && !this.props.force) {
this.setState({phase: PHASE_MIGRATE});
} else {
this.setState({phase: PHASE_INTRO});
_onSetUpClick = () => {
this.setState({phase: PHASE_PASSPHRASE});
}
_onSkipPassPhraseClick = async () => {
this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseNextClick = async (e) => {
e.preventDefault();
if (!this._passphraseField.current) return; // unmounting
await this._passphraseField.current.validate({ allowEmpty: false });
if (!this._passphraseField.current.state.valid) {
this._passphraseField.current.focus();
this._passphraseField.current.validate({ allowEmpty: false, focused: true });
return;
}
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
};
_onPassPhraseConfirmNextClick = async (e) => {
e.preventDefault();
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
}
_onSetAgainClick = () => {
this.setState({
passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '',
phase: PHASE_PASSPHRASE,
});
}
_onKeepItSafeBackClick = () => {
this.setState({
phase: PHASE_SHOWKEY,
});
}
_onPassPhraseValidate = (result) => {
this.setState({
passPhraseValid: result.valid,
});
};
_onPassPhraseChange = (e) => {
this.setState({
passPhrase: e.target.value,
});
}
_onPassPhraseConfirmChange = (e) => {
this.setState({
passPhraseConfirm: e.target.value,
});
}
_onAccountPasswordChange = (e) => {
@ -384,14 +433,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// Once we're confident enough in this (and it's supported enough) we can do
// it automatically.
// https://github.com/vector-im/riot-web/issues/11696
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Field = sdk.getComponent('views.elements.Field');
let authPrompt;
let nextCaption = _t("Next");
if (!this.state.backupSigStatus.usable) {
authPrompt = null;
nextCaption = _t("Upload");
} else if (this.state.canUploadKeysWithPasswordOnly && !this.props.accountPassword) {
if (this.state.canUploadKeysWithPasswordOnly) {
authPrompt = <div>
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
<div><Field
@ -403,6 +450,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
autoFocus={true}
/></div>
</div>;
} else if (!this.state.backupSigStatus.usable) {
authPrompt = <div>
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
</div>;
nextCaption = _t("Restore");
} else {
authPrompt = <p>
{_t("You'll need to authenticate with the server to confirm the upgrade.")}
@ -411,9 +463,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
return <form onSubmit={this._onMigrateFormSubmit}>
<p>{_t(
"Upgrade your Recovery Key to store encryption keys & secrets " +
"with your account data. If you lose access to this login you'll " +
"need it to unlock your data.",
"Upgrade this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them " +
"as trusted for other users.",
)}</p>
<div>{authPrompt}</div>
<DialogButtons
@ -429,49 +481,185 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</form>;
}
_renderPhaseShowKey() {
let continueButton;
if (this.state.phase === PHASE_SHOWKEY) {
continueButton = <DialogButtons primaryButton={_t("Continue")}
disabled={!this.state.downloaded && !this.state.copied}
onPrimaryButtonClick={this._onShowKeyContinueClick}
_renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t(
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
"This should be different to your account password:",
)}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField
className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this._onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
autoFocus={true}
label={_td("Enter a recovery passphrase")}
labelEnterPassword={_td("Enter a recovery passphrase")}
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
/>
</div>
<LabelledToggleSwitch
label={ _t("Back up encrypted message keys")}
onChange={this._onUseKeyBackupChange} value={this.state.useKeyBackup}
/>
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNextClick}
hasCancel={false}
/>;
} else {
continueButton = <div className="mx_CreateSecretStorageDialog_continueSpinner">
<InlineSpinner />
</div>;
disabled={!this.state.passPhraseValid}
>
<button type="button"
onClick={this._onSkipSetupClick}
className="danger"
>{_t("Skip")}</button>
</DialogButtons>
<details>
<summary>{_t("Advanced")}</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a recovery key")}
</AccessibleButton>
</details>
</form>;
}
_renderPhasePassPhraseConfirm() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Field = sdk.getComponent('views.elements.Field');
let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!");
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended
// on this screen, this would make it easy for a malicious person to guess
// your passphrase one letter at a time, but they could get this faster by
// just opening the browser's developer tools and reading it.
// Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint.
matchText = _t("That doesn't match.");
changeText = _t("Go back to set it again.");
}
let passPhraseMatch = null;
if (matchText) {
passPhraseMatch = <div>
<div>{matchText}</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{changeText}
</AccessibleButton>
</div>
</div>;
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
"Enter your recovery passphrase a second time to confirm it.",
)}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
type="password"
onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your recovery passphrase")}
autoFocus={true}
autoComplete="new-password"
/>
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
{passPhraseMatch}
</div>
</div>
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
>
<button type="button"
onClick={this._onSkipSetupClick}
className="danger"
>{_t("Skip")}</button>
</DialogButtons>
</form>;
}
_renderPhaseShowKey() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div>
<p>{_t(
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.",
"Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your recovery passphrase.",
)}</p>
<p>{_t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
)}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyHeader">
{_t("Your recovery key")}
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary"
onClick={this._onDownloadClick}
disabled={this.state.phase === PHASE_STORING}
>
{_t("Download")}
</AccessibleButton>
<span>{_t("or")}</span>
<AccessibleButton
kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick}
disabled={this.state.phase === PHASE_STORING}
>
{this.state.copied ? _t("Copied!") : _t("Copy")}
{_t("Copy")}
</AccessibleButton>
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
</AccessibleButton>
</div>
</div>
</div>
{continueButton}
</div>;
}
_renderPhaseKeepItSafe() {
let introText;
if (this.state.copied) {
introText = _t(
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your recovery key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{introText}
<ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
</ul>
<DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this._bootstrapSecretStorage}
hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
</div>;
}
@ -483,6 +671,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_renderPhaseLoadError() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t("Unable to query secret storage status")}</p>
<div className="mx_Dialog_buttons">
@ -495,44 +684,29 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>;
}
_renderPhaseIntro() {
let cancelButton;
if (this.props.force) {
// if this is a forced key reset then aborting will just leave the old keys
// in place, and is thereforece just 'cancel'
cancelButton = <button type="button" onClick={this._onCancel}>{_t('Cancel')}</button>;
} else {
// if it's setting up from scratch then aborting leaves the user without
// crypto set up, so they skipping the setup.
cancelButton = <button type="button"
className="danger" onClick={this._onSkipSetupClick}
>{_t('Skip')}</button>;
}
_renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"Create a Recovery Key to store encryption keys & secrets with your account data. " +
"If you lose access to this login youll need it to unlock your data.",
"You can now verify your other devices, " +
"and other users to keep your chats safe.",
)}</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onIntroContinueClick}
hasCancel={false}
>
{cancelButton}
</DialogButtons>
</div>
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
hasCancel={false}
/>
</div>;
}
_renderPhaseSkipConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{_t(
"Without completing security on this session, it wont have " +
"access to encrypted messages.",
)}
<DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this._onGoBackClick}
onPrimaryButtonClick={this._onSetUpClick}
hasCancel={false}
>
<button type="button" className="danger" onClick={this._onCancel}>{_t('Skip')}</button>
@ -542,15 +716,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_titleForPhase(phase) {
switch (phase) {
case PHASE_INTRO:
return _t('Create a Recovery Key');
case PHASE_MIGRATE:
return _t('Upgrade your Recovery Key');
return _t('Upgrade your encryption');
case PHASE_PASSPHRASE:
return _t('Set up encryption');
case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm recovery passphrase');
case PHASE_CONFIRM_SKIP:
return _t('Are you sure?');
case PHASE_SHOWKEY:
case PHASE_KEEPITSAFE:
return _t('Make a copy of your recovery key');
case PHASE_STORING:
return _t('Store your Recovery Key');
return _t('Setting up keys');
case PHASE_DONE:
return _t("You're done!");
default:
return '';
}
@ -561,6 +741,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let content;
if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div>
<p>{_t("Unable to set up secret storage")}</p>
<div className="mx_Dialog_buttons">
@ -579,16 +760,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
case PHASE_LOADERROR:
content = this._renderPhaseLoadError();
break;
case PHASE_INTRO:
content = this._renderPhaseIntro();
break;
case PHASE_MIGRATE:
content = this._renderPhaseMigrate();
break;
case PHASE_PASSPHRASE:
content = this._renderPhasePassPhrase();
break;
case PHASE_PASSPHRASE_CONFIRM:
content = this._renderPhasePassPhraseConfirm();
break;
case PHASE_SHOWKEY:
case PHASE_STORING:
content = this._renderPhaseShowKey();
break;
case PHASE_KEEPITSAFE:
content = this._renderPhaseKeepItSafe();
break;
case PHASE_STORING:
content = this._renderBusyPhase();
break;
case PHASE_DONE:
content = this._renderPhaseDone();
break;
case PHASE_CONFIRM_SKIP:
content = this._renderPhaseSkipConfirm();
break;
@ -605,7 +797,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
headerImage={headerImage}
hasCancel={this.props.hasCancel}
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
fixedWidth={false}
>
<div>

View file

@ -35,15 +35,15 @@ export interface ISelectionRange {
export interface ICompletion {
type: "at-room" | "command" | "community" | "room" | "user";
completion: string,
completion: string;
completionId?: string;
component?: ReactElement,
range: ISelectionRange,
command?: string,
component?: ReactElement;
range: ISelectionRange;
command?: string;
suffix?: string;
// If provided, apply a LINK entity to the completion with the
// data = { url: href }.
href?: string,
href?: string;
}
const PROVIDERS = [

View file

@ -46,7 +46,7 @@ export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props
});
interface IPillCompletionProps extends ITextualCompletionProps {
children?: React.ReactNode,
children?: React.ReactNode;
}
export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref) => {

View file

@ -17,8 +17,6 @@ limitations under the License.
*/
import _at from 'lodash/at';
import _flatMap from 'lodash/flatMap';
import _sortBy from 'lodash/sortBy';
import _uniq from 'lodash/uniq';
function stripDiacritics(str: string): string {
@ -35,8 +33,9 @@ interface IOptions<T extends {}> {
/**
* Simple search matcher that matches any results with the query string anywhere
* in the search string. Returns matches in the order the query string appears
* in the search key, earliest first, then in the order the items appeared in
* the source array.
* in the search key, earliest first, then in the order the search key appears
* in the provided array of keys, then in the order the items appeared in the
* source array.
*
* @param {Object[]} objects Initial list of objects. Equivalent to calling
* setObjects() after construction
@ -49,7 +48,7 @@ export default class QueryMatcher<T extends Object> {
private _options: IOptions<T>;
private _keys: IOptions<T>["keys"];
private _funcs: Required<IOptions<T>["funcs"]>;
private _items: Map<string, T[]>;
private _items: Map<string, {object: T, keyWeight: number}[]>;
constructor(objects: T[], options: IOptions<T> = { keys: [] }) {
this._options = options;
@ -85,13 +84,16 @@ export default class QueryMatcher<T extends Object> {
keyValues.push(f(object));
}
for (const keyValue of keyValues) {
for (const [index, keyValue] of Object.entries(keyValues)) {
if (!keyValue) continue; // skip falsy keyValues
const key = stripDiacritics(keyValue).toLowerCase();
if (!this._items.has(key)) {
this._items.set(key, []);
}
this._items.get(key).push(object);
this._items.get(key).push({
keyWeight: Number(index),
object,
});
}
}
}
@ -104,32 +106,40 @@ export default class QueryMatcher<T extends Object> {
if (query.length === 0) {
return [];
}
const results = [];
const matches = [];
// Iterate through the map & check each key.
// ES6 Map iteration order is defined to be insertion order, so results
// here will come out in the order they were put in.
for (const key of this._items.keys()) {
for (const [key, candidates] of this._items.entries()) {
let resultKey = key;
if (this._options.shouldMatchWordsOnly) {
resultKey = resultKey.replace(/[^\w]/g, '');
}
const index = resultKey.indexOf(query);
if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) {
results.push({key, index});
matches.push(
...candidates.map((candidate) => ({index, ...candidate}))
);
}
}
// Sort them by where the query appeared in the search key
// lodash sortBy is a stable sort, so results where the query
// appeared in the same place will retain their order with
// respect to each other.
const sortedResults = _sortBy(results, (candidate) => {
return candidate.index;
// Sort matches by where the query appeared in the search key, then by
// where the matched key appeared in the provided array of keys.
matches.sort((a, b) => {
if (a.index < b.index) {
return -1;
} else if (a.index === b.index) {
if (a.keyWeight < b.keyWeight) {
return -1;
} else if (a.keyWeight === b.keyWeight) {
return 0;
}
}
return 1;
});
// Now map the keys to the result objects. Each result object is a list, so
// flatMap will flatten those lists out into a single list. Also remove any
// duplicates.
return _uniq(_flatMap(sortedResults, (candidate) => this._items.get(candidate.key)));
// Now map the keys to the result objects. Also remove any duplicates.
return _uniq(matches.map((match) => match.object));
}
}

View file

@ -108,6 +108,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `${headerStickyWidth}px`;
header.style.top = `unset`;
gotBottom = true;
} else if (slRect.top < top) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
@ -119,6 +120,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `unset`;
header.style.top = `unset`;
}
}
};

View file

@ -151,9 +151,9 @@ interface IProps { // TODO type things better
// Represents the screen to display as a result of parsing the initial window.location
initialScreenAfterLogin?: IScreen;
// displayname, if any, to set on the device when logging in/registering.
defaultDeviceDisplayName?: string,
defaultDeviceDisplayName?: string;
// A function that makes a registration URL
makeRegistrationUrl: (object) => string,
makeRegistrationUrl: (object) => string;
}
interface IState {
@ -1870,42 +1870,35 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.accountPasswordTimer = null;
}, 60 * 5 * 1000);
// Wait for the client to be logged in (but not started)
// which is enough to ask the server about account data.
const loggedIn = new Promise(resolve => {
const actionHandlerRef = dis.register(payload => {
if (payload.action !== "on_logged_in") {
return;
}
dis.unregister(actionHandlerRef);
resolve();
});
});
// Create and start the client in the background
const setLoggedInPromise = Lifecycle.setLoggedIn(credentials);
await loggedIn;
// Create and start the client
await Lifecycle.setLoggedIn(credentials);
const cli = MatrixClientPeg.get();
// We're checking `isCryptoAvailable` here instead of `isCryptoEnabled`
// because the client hasn't been started yet.
const cryptoAvailable = isCryptoAvailable();
if (!cryptoAvailable) {
const cryptoEnabled = cli.isCryptoEnabled();
if (!cryptoEnabled) {
this.onLoggedIn();
}
this.setState({ pendingInitialSync: true });
await this.firstSyncPromise.promise;
if (!cryptoAvailable) {
this.setState({ pendingInitialSync: false });
return setLoggedInPromise;
const promisesList = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account
promisesList.push(cli.downloadKeys([cli.getUserId()]));
}
// Test for the master cross-signing key in SSSS as a quick proxy for
// whether cross-signing has been set up on the account.
const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master");
if (masterKeyInStorage) {
// Now update the state to say we're waiting for the first sync to complete rather
// than for the login to finish.
this.setState({ pendingInitialSync: true });
await Promise.all(promisesList);
if (!cryptoEnabled) {
this.setState({ pendingInitialSync: false });
return;
}
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
if (crossSigningIsSetUp) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
@ -1913,8 +1906,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.onLoggedIn();
}
this.setState({ pendingInitialSync: false });
return setLoggedInPromise;
};
// complete security / e2e setup has finished

View file

@ -39,7 +39,7 @@ import Tinter from '../../Tinter';
import rate_limited_func from '../../ratelimitedfunc';
import * as ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms';
import eventSearch from '../../Searching';
import eventSearch, {searchPagination} from '../../Searching';
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
@ -1036,8 +1036,7 @@ export default createReactClass({
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
const searchPromise = this.context.backPaginateRoomEventsSearch(
this.state.searchResults);
const searchPromise = searchPagination(this.state.searchResults);
return this._handleSearchResult(searchPromise);
} else {
debuglog("no more search results");
@ -1314,6 +1313,14 @@ export default createReactClass({
const mxEv = result.context.getEvent();
const roomId = mxEv.getRoomId();
const room = this.context.getRoom(roomId);
if (!room) {
// if we do not have the room in js-sdk stores then hide it as we cannot easily show it
// As per the spec, an all rooms search can create this condition,
// it happens with Seshat but not Synapse.
// It will make the result count not match the displayed count.
console.log("Hiding search result from an unknown room", roomId);
continue;
}
if (!haveTileForEvent(mxEv)) {
// XXX: can this ever happen? It will make the result count
@ -1322,16 +1329,9 @@ export default createReactClass({
}
if (this.state.searchScope === 'All') {
if (roomId != lastRoomId) {
// XXX: if we've left the room, we might not know about
// it. We should tell the js sdk to go and find out about
// it. But that's not an issue currently, as synapse only
// returns results for rooms we're joined to.
const roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
if (roomId !== lastRoomId) {
ret.push(<li key={mxEv.getId() + "-room"}>
<h2>{ _t("Room") }: { roomName }</h2>
<h2>{ _t("Room") }: { room.name }</h2>
</li>);
lastRoomId = roomId;
}

View file

@ -291,6 +291,6 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
</ContextMenuButton>
{contextMenu}
</React.Fragment>
)
);
}
}

View file

@ -21,7 +21,6 @@ import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_RECOVERY_KEY,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
@ -62,9 +61,6 @@ export default class CompleteSecurity extends React.Component {
if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
} else if (phase === PHASE_RECOVERY_KEY) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Recovery Key");
} else if (phase === PHASE_DONE) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Session verified");

View file

@ -378,7 +378,7 @@ export default createReactClass({
}
if (response.access_token) {
const cli = await this.props.onLoggedIn({
await this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
@ -386,7 +386,7 @@ export default createReactClass({
accessToken: response.access_token,
}, this.state.formVals.password);
this._setupPushers(cli);
this._setupPushers();
// we're still busy until we get unmounted: don't show the registration form again
newState.busy = true;
} else {
@ -397,10 +397,11 @@ export default createReactClass({
this.setState(newState);
},
_setupPushers: function(matrixClient) {
_setupPushers: function() {
if (!this.props.brand) {
return Promise.resolve();
}
const matrixClient = MatrixClientPeg.get();
return matrixClient.getPushers().then((resp)=>{
const pushers = resp.pushers;
for (let i = 0; i < pushers.length; ++i) {

View file

@ -19,12 +19,9 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import withValidation from '../../views/elements/Validation';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_RECOVERY_KEY,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
@ -56,11 +53,6 @@ export default class SetupEncryptionBody extends React.Component {
// Because of the latter, it lives in the state.
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
recoveryKey: '',
// whether the recovery key is a valid recovery key
recoveryKeyValid: null,
// whether the recovery key is the correct key or not
recoveryKeyCorrect: null,
};
}
@ -83,19 +75,9 @@ export default class SetupEncryptionBody extends React.Component {
store.stop();
}
_onResetClick = () => {
_onUsePassphraseClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.startKeyReset();
}
_onUseRecoveryKeyClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.useRecoveryKey();
}
_onRecoveryKeyCancelClick() {
const store = SetupEncryptionStore.sharedInstance();
store.cancelUseRecoveryKey();
store.usePassPhrase();
}
onSkipClick = () => {
@ -118,66 +100,6 @@ export default class SetupEncryptionBody extends React.Component {
store.done();
}
_onUsePassphraseClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.usePassPhrase();
}
_onRecoveryKeyChange = (e) => {
this.setState({recoveryKey: e.target.value});
}
_onRecoveryKeyValidate = async (fieldState) => {
const result = await this._validateRecoveryKey(fieldState);
this.setState({recoveryKeyValid: result.valid});
return result;
}
_validateRecoveryKey = withValidation({
rules: [
{
key: "required",
test: async (state) => {
try {
const decodedKey = decodeRecoveryKey(state.value);
const correct = await MatrixClientPeg.get().checkSecretStorageKey(
decodedKey, SetupEncryptionStore.sharedInstance().keyInfo,
);
this.setState({
recoveryKeyValid: true,
recoveryKeyCorrect: correct,
});
return correct;
} catch (e) {
this.setState({
recoveryKeyValid: false,
recoveryKeyCorrect: false,
});
return false;
}
},
invalid: function() {
if (this.state.recoveryKeyValid) {
return _t("This isn't the recovery key for your account");
} else {
return _t("This isn't a valid recovery key");
}
},
valid: function() {
return _t("Looks good!");
},
},
],
})
_onRecoveryKeyFormSubmit = (e) => {
e.preventDefault();
if (!this.state.recoveryKeyCorrect) return;
const store = SetupEncryptionStore.sharedInstance();
store.setupWithRecoveryKey(decodeRecoveryKey(this.state.recoveryKey));
}
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@ -196,11 +118,19 @@ export default class SetupEncryptionBody extends React.Component {
} else if (phase === PHASE_INTRO) {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (keyHasPassphrase(store.keyInfo)) {
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
} else {
} else if (store.keyInfo) {
recoveryKeyPrompt = _t("Use Recovery Key");
}
let useRecoveryKeyButton;
if (recoveryKeyPrompt) {
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
{recoveryKeyPrompt}
</AccessibleButton>;
}
return (
<div>
<p>{_t(
@ -224,67 +154,13 @@ export default class SetupEncryptionBody extends React.Component {
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="link" onClick={this._onUseRecoveryKeyClick}>
{recoveryKeyPrompt}
</AccessibleButton>
{useRecoveryKeyButton}
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")}
</AccessibleButton>
</div>
<div className="mx_CompleteSecurity_resetText">{_t(
"If you've forgotten your recovery key you can " +
"<button>set up new recovery options</button>", {}, {
button: sub => <AccessibleButton
element="span" className="mx_linkButton" onClick={this._onResetClick}
>
{sub}
</AccessibleButton>,
},
)}</div>
</div>
);
} else if (phase === PHASE_RECOVERY_KEY) {
const store = SetupEncryptionStore.sharedInstance();
let keyPrompt;
if (keyHasPassphrase(store.keyInfo)) {
keyPrompt = _t(
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.", {},
{
a: sub => <AccessibleButton
element="span"
className="mx_linkButton"
onClick={this._onUsePassphraseClick}
>{sub}</AccessibleButton>,
},
);
} else {
keyPrompt = _t("Enter your Recovery Key to continue.");
}
const Field = sdk.getComponent('elements.Field');
return <form onSubmit={this._onRecoveryKeyFormSubmit}>
<p>{keyPrompt}</p>
<div className="mx_CompleteSecurity_recoveryKeyEntry">
<Field
type="text"
label={_t('Recovery Key')}
value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange}
onValidate={this._onRecoveryKeyValidate}
/>
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="secondary" onClick={this._onRecoveryKeyCancelClick}>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton kind="primary"
disabled={!this.state.recoveryKeyCorrect}
onClick={this._onRecoveryKeyFormSubmit}
>
{_t("Continue")}
</AccessibleButton>
</div>
</form>;
} else if (phase === PHASE_DONE) {
let message;
if (this.state.backupInfo) {

View file

@ -118,7 +118,7 @@ class PassphraseField extends PureComponent<IProps, IState> {
value={this.props.value}
onChange={this.props.onChange}
onValidate={this.onValidate}
/>
/>;
}
}

View file

@ -88,7 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
_onResetRecoveryClick = () => {
this.props.onFinished(false);
accessSecretStorage(() => {}, {forceReset: true});
accessSecretStorage(() => {}, /* forceReset = */ true);
}
_onRecoveryKeyChange = (e) => {

View file

@ -32,9 +32,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
keyInfo: PropTypes.object.isRequired,
// Function from one of { passphrase, recoveryKey } -> boolean
checkPrivateKey: PropTypes.func.isRequired,
// If true, only prompt for a passphrase and do not offer to restore with
// a recovery key or reset keys.
passphraseOnly: PropTypes.bool,
}
constructor(props) {
@ -61,7 +58,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
_onResetRecoveryClick = () => {
// Re-enter the access flow, but resetting storage this time around.
this.props.onFinished(false);
accessSecretStorage(() => {}, {forceReset: true});
accessSecretStorage(() => {}, /* forceReset = */ true);
}
_onRecoveryKeyChange = (e) => {
@ -167,7 +164,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
primaryDisabled={this.state.passPhrase.length === 0}
/>
</form>
{this.props.passphraseOnly ? null : _t(
{_t(
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
@ -237,7 +234,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
primaryDisabled={!this.state.recoveryKeyValid}
/>
</form>
{this.props.passphraseOnly ? null : _t(
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."
, {}, {

View file

@ -19,7 +19,7 @@ import React from 'react';
import {Key} from '../../../Keyboard';
import classnames from 'classnames';
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>;
/**
* children: React's magic prop. Represents all children given to the element.
@ -40,7 +40,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
disabled?: boolean;
className?: string;
onClick?(e?: ButtonEvent): void;
};
}
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
ref?: React.Ref<Element>;

View file

@ -17,20 +17,20 @@ limitations under the License.
import React from 'react';
interface IProps {
className: string,
dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState,
onMouseUp: (event: MouseEvent) => void,
className: string;
dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState;
onMouseUp: (event: MouseEvent) => void;
}
interface IState {
onMouseMove: (event: MouseEvent) => void,
onMouseUp: (event: MouseEvent) => void,
location: ILocationState,
onMouseMove: (event: MouseEvent) => void;
onMouseUp: (event: MouseEvent) => void;
location: ILocationState;
}
export interface ILocationState {
currentX: number,
currentY: number,
currentX: number;
currentY: number;
}
export default class Draggable extends React.Component<IProps, IState> {
@ -58,13 +58,13 @@ export default class Draggable extends React.Component<IProps, IState> {
document.addEventListener("mousemove", this.state.onMouseMove);
document.addEventListener("mouseup", this.state.onMouseUp);
}
};
private onMouseUp = (event: MouseEvent): void => {
document.removeEventListener("mousemove", this.state.onMouseMove);
document.removeEventListener("mouseup", this.state.onMouseUp);
this.props.onMouseUp(event);
}
};
private onMouseMove(event: MouseEvent): void {
const newLocation = this.props.dragFunc(this.state.location, event);
@ -75,7 +75,7 @@ export default class Draggable extends React.Component<IProps, IState> {
}
render() {
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />;
}
}

View file

@ -87,20 +87,20 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElem
type PropShapes = IInputProps | ISelectProps | ITextareaProps;
interface IState {
valid: boolean,
feedback: React.ReactNode,
feedbackVisible: boolean,
focused: boolean,
valid: boolean;
feedback: React.ReactNode;
feedbackVisible: boolean;
focused: boolean;
}
export default class Field extends React.PureComponent<PropShapes, IState> {
private id: string;
private input: HTMLInputElement;
private static defaultProps = {
public static readonly defaultProps = {
element: "input",
type: "text",
}
};
/*
* This was changed from throttle to debounce: this is more traditional for

View file

@ -20,15 +20,15 @@ import Draggable, {ILocationState} from './Draggable';
interface IProps {
// Current room
roomId: string,
minWidth: number,
maxWidth: number,
};
roomId: string;
minWidth: number;
maxWidth: number;
}
interface IState {
width: number,
IRCLayoutRoot: HTMLElement,
};
width: number;
IRCLayoutRoot: HTMLElement;
}
export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
constructor(props: IProps) {
@ -37,20 +37,19 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
this.state = {
width: SettingsStore.getValue("ircDisplayNameWidth", this.props.roomId),
IRCLayoutRoot: null,
}
};
};
}
componentDidMount() {
this.setState({
IRCLayoutRoot: document.querySelector(".mx_IRCLayout") as HTMLElement,
}, () => this.updateCSSWidth(this.state.width))
}, () => this.updateCSSWidth(this.state.width));
}
private dragFunc = (location: ILocationState, event: React.MouseEvent<Element, MouseEvent>): ILocationState => {
const offset = event.clientX - location.currentX;
const newWidth = this.state.width + offset;
console.log({offset})
// If we're trying to go smaller than min width, don't.
if (newWidth < this.props.minWidth) {
return location;
@ -69,8 +68,8 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
return {
currentX: event.clientX,
currentY: location.currentY,
}
}
};
};
private updateCSSWidth(newWidth: number) {
this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px");
@ -83,6 +82,10 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
}
render() {
return <Draggable className="mx_ProfileResizer" dragFunc={this.dragFunc.bind(this)} onMouseUp={this.onMoueUp.bind(this)}/>
return <Draggable
className="mx_ProfileResizer"
dragFunc={this.dragFunc.bind(this)}
onMouseUp={this.onMoueUp.bind(this)}
/>;
}
};
}

View file

@ -48,18 +48,18 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
this.props.roomId,
this.props.isExplicit,
),
}
};
}
private onChange = (checked: boolean): void => {
this.save(checked);
this.setState({ value: checked });
if (this.props.onChange) this.props.onChange(checked);
}
};
private checkBoxOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.onChange(e.target.checked);
}
};
private save = (val?: boolean): void => {
return SettingsStore.setValue(
@ -68,7 +68,7 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
this.props.level,
val !== undefined ? val : this.state.value,
);
}
};
public render() {
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);

View file

@ -65,9 +65,9 @@ export default class Slider extends React.Component<IProps> {
const intervalWidth = 1 / (values.length - 1);
const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue)
const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue);
return 100 * (closest - 1 + linearInterpolation) * intervalWidth
return 100 * (closest - 1 + linearInterpolation) * intervalWidth;
}
@ -87,7 +87,7 @@ export default class Slider extends React.Component<IProps> {
selection = <div className="mx_Slider_selection">
<div className="mx_Slider_selectionDot" style={{left: "calc(-0.55em + " + offset + "%)"}} />
<hr style={{width: offset + "%"}} />
</div>
</div>;
}
return <div className="mx_Slider">
@ -115,13 +115,13 @@ export default class Slider extends React.Component<IProps> {
interface IDotProps {
// Callback for behavior onclick
onClick: () => void,
onClick: () => void;
// Whether the dot should appear active
active: boolean,
active: boolean;
// The label on the dot
label: string,
label: string;
// Whether the slider is disabled
disabled: boolean;
@ -129,7 +129,7 @@ interface IDotProps {
class Dot extends React.PureComponent<IDotProps> {
render(): React.ReactNode {
let className = "mx_Slider_dot"
let className = "mx_Slider_dot";
if (!this.props.disabled && this.props.active) {
className += " mx_Slider_dotActive";
}

View file

@ -30,7 +30,7 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
public static readonly defaultProps = {
className: "",
}
};
constructor(props: IProps) {
super(props);
@ -51,6 +51,6 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
{ this.props.children }
</div>
</label>
</span>
</span>;
}
}

View file

@ -26,7 +26,7 @@ interface IState {
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
public static readonly defaultProps = {
className: '',
}
};
public render() {
const { children, className, disabled, ...otherProps } = this.props;
@ -43,6 +43,6 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
<div><div></div></div>
<span>{children}</span>
<div className="mx_RadioButton_spacer" />
</label>
</label>;
}
}

View file

@ -28,7 +28,7 @@ interface IProps {
// Called when the checked state changes. First argument will be the new state.
onChange(checked: boolean): void;
};
}
// Controlled Toggle Switch element, written with Accessibility in mind
export default ({checked, disabled = false, onChange, ...props}: IProps) => {

View file

@ -29,15 +29,15 @@ const MIN_TOOLTIP_HEIGHT = 25;
interface IProps {
// Class applied to the element used to position the tooltip
className: string,
className: string;
// Class applied to the tooltip itself
tooltipClassName?: string,
tooltipClassName?: string;
// Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset.
visible?: boolean,
visible?: boolean;
// the react element to put into the tooltip
label: React.ReactNode,
label: React.ReactNode;
}
export default class Tooltip extends React.Component<IProps> {
@ -126,7 +126,7 @@ export default class Tooltip extends React.Component<IProps> {
tooltip: this.tooltip,
parent: parent,
});
}
};
public render() {
// Render a placeholder

View file

@ -240,6 +240,7 @@ export class ListNotificationState extends EventEmitter implements IDestroyable
this.rooms = rooms;
for (const oldRoom of diff.removed) {
const state = this.states[oldRoom.roomId];
if (!state) continue; // We likely just didn't have a badge (race condition)
delete this.states[oldRoom.roomId];
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
state.destroy();

View file

@ -97,7 +97,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
>
<RoomAvatar room={r} width={32} height={32}/>
</AccessibleButton>
)
);
});
if (tiles.length > 0) {

View file

@ -41,6 +41,11 @@ import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorith
* warning disappears. *
*******************************************************************/
const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
interface IProps {
forRooms: boolean;
rooms?: Room[];
@ -105,7 +110,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private onShowAllClick = () => {
this.props.layout.visibleTiles = this.numTiles;
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
@ -359,7 +364,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
{showMoreText}
</div>
);
} else if (tiles.length <= nVisible) {
} else if (tiles.length <= nVisible && tiles.length > this.props.layout.minVisibleTiles) {
// we have all tiles visible - add a button to show less
let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'>
@ -393,18 +398,16 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// goes backwards and can become wildly incorrect (visibleTiles says 18 when there's
// only mathematically 7 possible).
const showMoreHeight = 32; // As defined by CSS
const resizeHandleHeight = 4; // As defined by CSS
// The padding is variable though, so figure out what we need padding for.
let padding = 0;
if (showNButton) padding += showMoreHeight;
if (handles.length > 0) padding += resizeHandleHeight;
if (showNButton) padding += SHOW_N_BUTTON_HEIGHT;
padding += RESIZE_HANDLE_HEIGHT; // always append the handle height
const minTilesPx = layout.calculateTilesToPixelsMin(tiles.length, layout.minVisibleTiles, padding);
const relativeTiles = layout.tilesWithPadding(tiles.length, padding);
const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding);
const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding);
const tilesWithoutPadding = Math.min(tiles.length, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(tiles.length, tilesWithoutPadding, padding);
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
content = (
<ResizableBox
@ -420,7 +423,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
{visibleTiles}
{showNButton}
</ResizableBox>
)
);
}
// TODO: onKeyDown support

View file

@ -232,7 +232,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
/>
{contextMenu}
</React.Fragment>
)
);
}
public render(): React.ReactElement {

View file

@ -113,7 +113,7 @@ export default class CrossSigningPanel extends React.PureComponent {
_bootstrapSecureSecretStorage = async (forceReset=false) => {
this.setState({ error: null });
try {
await accessSecretStorage(() => undefined, {forceReset});
await accessSecretStorage(() => undefined, forceReset);
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping secret storage", e);

View file

@ -34,14 +34,14 @@ interface IProps {
}
interface IThemeState {
theme: string,
useSystemTheme: boolean,
theme: string;
useSystemTheme: boolean;
}
export interface CustomThemeMessage {
isError: boolean,
text: string
};
isError: boolean;
text: string;
}
interface IState extends IThemeState {
// String displaying the current selected fontSize.
@ -164,7 +164,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
);
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
}
};
private onAddCustomTheme = async (): Promise<void> => {
let currentThemes: string[] = SettingsStore.getValue("custom_themes");

View file

@ -90,7 +90,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
} catch (err) {
console.error("Error while cancelling verification request", err);
}
}
};
accept = async () => {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);

View file

@ -19,7 +19,7 @@ import { Action } from "../actions";
import {UpdateCheckStatus} from "../../BasePlatform";
export interface CheckUpdatesPayload extends ActionPayload {
action: Action.CheckUpdates,
action: Action.CheckUpdates;
/**
* The current phase of the manual update check.

View file

@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface OpenToTabPayload extends ActionPayload {
action: Action.ViewUserSettings | string, // TODO: Add room settings action
action: Action.ViewUserSettings | string; // TODO: Add room settings action
/**
* The tab ID to open in the settings view to start, if possible.

View file

@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface RecheckThemePayload extends ActionPayload {
action: Action.RecheckTheme,
action: Action.RecheckTheme;
/**
* Optionally specify the exact theme which is to be loaded.

View file

@ -19,7 +19,7 @@ import { Action } from "../actions";
import { Component } from "react";
export interface ViewTooltipPayload extends ActionPayload {
action: Action.ViewTooltip,
action: Action.ViewTooltip;
/*
* The tooltip to render. If it's null the tooltip will not be rendered
@ -31,5 +31,5 @@ export interface ViewTooltipPayload extends ActionPayload {
* The parent under which to render the tooltip. Can be null to remove
* the parent type.
*/
parent: null | Element
parent: null | Element;
}

View file

@ -19,7 +19,7 @@ import { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface ViewUserPayload extends ActionPayload {
action: Action.ViewUser,
action: Action.ViewUser;
/**
* The member to view. May be null or falsy to indicate that no member

View file

@ -2242,8 +2242,8 @@
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Beim Ändern der Benutzerrechte ist ein Fehler aufgetreten. Stelle sicher dass du die nötigen Berechtigungen besitzt und versuche es erneut.",
"Unable to share email address": "E-Mail Adresse konnte nicht geteilt werden",
"Please enter verification code sent via text.": "Gib den Verifikationscode ein, den du empfangen hast.",
"Almost there! Is your other session showing the same shield?": "Fast geschafft! Zeigt deine andere Sitzung die gleichen Zeichen?",
"Almost there! Is %(displayName)s showing the same shield?": "Fast geschafft! Werden bei %(displayName)s die gleichen Zeichen angezeigt?",
"Almost there! Is your other session showing the same shield?": "Fast geschafft! Zeigt deine andere Sitzung das gleiche Schild?",
"Almost there! Is %(displayName)s showing the same shield?": "Fast geschafft! Wird bei %(displayName)s das gleiche Schild angezeigt?",
"Click the link in the email you received to verify and then click continue again.": "Klicke auf den Link in der Bestätigungs-E-Mail, und dann auf Weiter.",
"Unable to revoke sharing for phone number": "Widerrufen der geteilten Telefonnummer nicht möglich",
"Unable to share phone number": "Teilen der Telefonnummer nicht möglich",

View file

@ -433,7 +433,7 @@
"Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers": "Multiple integration managers",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
"Use the improved room list (in development - will refresh to apply changes)": "Use the improved room list (in development - will refresh to apply changes)",
"Use the improved room list (will refresh to apply changes)": "Use the improved room list (will refresh to apply changes)",
"Support adding custom themes": "Support adding custom themes",
"Use IRC layout": "Use IRC layout",
"Show info about bridges in room settings": "Show info about bridges in room settings",
@ -2040,7 +2040,6 @@
"Search failed": "Search failed",
"Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(",
"No more results": "No more results",
"Unknown room %(roomId)s": "Unknown room %(roomId)s",
"Room": "Room",
"Failed to reject invite": "Failed to reject invite",
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
@ -2069,7 +2068,6 @@
"Account settings": "Account settings",
"Could not load user profile": "Could not load user profile",
"Verify this login": "Verify this login",
"Recovery Key": "Recovery Key",
"Session verified": "Session verified",
"Failed to send email": "Failed to send email",
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
@ -2123,16 +2121,11 @@
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
"Registration Successful": "Registration Successful",
"Create your account": "Create your account",
"This isn't the recovery key for your account": "This isn't the recovery key for your account",
"This isn't a valid recovery key": "This isn't a valid recovery key",
"Looks good!": "Looks good!",
"Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
"Use Recovery Key": "Use Recovery Key",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",
"This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:",
"or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.",
"Enter your Recovery Key to continue.": "Enter your Recovery Key to continue.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.",
@ -2176,43 +2169,47 @@
"Confirm encryption setup": "Confirm encryption setup",
"Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.",
"Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:",
"Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption",
"Restore": "Restore",
"You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.",
"Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.",
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.",
"Download": "Download",
"Copy": "Copy",
"Unable to query secret storage status": "Unable to query secret storage status",
"Retry": "Retry",
"Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login youll need it to unlock your data.": "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login youll need it to unlock your data.",
"Create a Recovery Key": "Create a Recovery Key",
"Upgrade your Recovery Key": "Upgrade your Recovery Key",
"Store your Recovery Key": "Store your Recovery Key",
"Unable to set up secret storage": "Unable to set up secret storage",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
"Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:",
"Enter a recovery passphrase": "Enter a recovery passphrase",
"Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.",
"Back up encrypted message keys": "Back up encrypted message keys",
"Set up with a recovery key": "Set up with a recovery key",
"That matches!": "That matches!",
"Use a different passphrase?": "Use a different passphrase?",
"That doesn't match.": "That doesn't match.",
"Go back to set it again.": "Go back to set it again.",
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
"Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.",
"Confirm your recovery passphrase": "Confirm your recovery passphrase",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.",
"Your recovery key": "Your recovery key",
"Copy": "Copy",
"Download": "Download",
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:": "Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
"Your recovery key is in your <b>Downloads</b> folder.": "Your recovery key is in your <b>Downloads</b> folder.",
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
"Unable to query secret storage status": "Unable to query secret storage status",
"Retry": "Retry",
"You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.",
"Upgrade your encryption": "Upgrade your encryption",
"Confirm recovery passphrase": "Confirm recovery passphrase",
"Make a copy of your recovery key": "Make a copy of your recovery key",
"You're done!": "You're done!",
"Unable to set up secret storage": "Unable to set up secret storage",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.",
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
"Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase",
"Confirm your recovery passphrase": "Confirm your recovery passphrase",
"Make a copy of your recovery key": "Make a copy of your recovery key",
"Starting backup...": "Starting backup...",
"Success!": "Success!",
"Create key backup": "Create key backup",

View file

@ -2374,7 +2374,7 @@
"Signing In...": "Salutante…",
"If you've joined lots of rooms, this might take a while": "Se vi aliĝis al multaj ĉambroj, tio povas daŭri longe",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Konfirmu vian identecon per kontrolo de ĉi tiu saluto el unu el viaj aliaj salutaĵoj, permesante al ĝi legadon de ĉifritaj mesaĝoj.",
"This requires the latest Riot on your other devices:": "Ĉi tio bezonas la plej freŝan version de Rion sur viaj aliaj aparatoj:",
"This requires the latest Riot on your other devices:": "Ĉi tio bezonas la plej freŝan version de Riot en viaj aliaj aparatoj:",
"or another cross-signing capable Matrix client": "aŭ alian Matrix-klienton kapablan je transiraj subskriboj",
"Use Recovery Passphrase or Key": "Uzi rehavajn pasfrazon aŭ ŝlosilon",
"Great! This recovery passphrase looks strong enough.": "Bonege! Ĉi tiu rehava pasfrazo ŝajnas sufiĉe forta.",

View file

@ -129,7 +129,7 @@
"Matrix rooms": "Matrix'i jututoad",
"Explore Room State": "Uuri jututoa olekut",
"Explore Account Data": "Uuri konto andmeid",
"Private Chat": "Privaatne vestlus",
"Private Chat": "Omavaheline privaatne vestlus",
"Public Chat": "Avalik vestlus",
"Other users can invite you to rooms using your contact details": "Teades sinu kontaktinfot võivad teised kutsuda sind osalema jututubades",
"Add rooms to the community summary": "Lisa jututoad kogukonna ülevaatesse",
@ -147,7 +147,7 @@
"Indexed rooms:": "Indekseeritud jututoad:",
"Remove": "Eemalda",
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Sa peaksid enne ühenduse katkestamisst <b>eemaldama isiklikud andmed</b> id-serverist <idserver />. Kahjuks id-server <idserver /> ei ole hetkel võrgus või pole kättesaadav.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Me soovitame, et eemaldad enne ühenduse katkestamist oma e-posti aadressi ja telefoninumbrid id-serverist.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Me soovitame, et eemaldad enne ühenduse katkestamist oma e-posti aadressi ja telefoninumbrid isikutuvastusserverist.",
"Remove messages": "Kustuta sõnumeid",
"Unable to remove contact information": "Kontaktiinfo eemaldamine ebaõnnestus",
"Remove %(email)s?": "Eemalda %(email)s?",
@ -515,7 +515,7 @@
"Displays information about a user": "Näitab teavet kasutaja kohta",
"This homeserver has hit its Monthly Active User limit.": "See koduserver on saavutanud igakuise aktiivsete kasutajate piiri.",
"about a minute ago": "umbes minut tagasi",
"about an hour ago": "umbes tund tagasi",
"about an hour ago": "umbes tund aega tagasi",
"%(num)s hours ago": "%(num)s tundi tagasi",
"about a day ago": "umbes päev tagasi",
"%(num)s days ago": "%(num)s päeva tagasi",
@ -1479,5 +1479,118 @@
"Enter recovery key": "Sisesta taastevõti",
"Unable to access secret storage. Please verify that you entered the correct recovery key.": "Ei õnnestu lugeda krüptitud salvestusruumi. Palun kontrolli, kas sa sisestasid õige taastevõtme.",
"This looks like a valid recovery key!": "See tundub olema õige taastevõti!",
"Not a valid recovery key": "Ei ole sobilik taastevõti"
"Not a valid recovery key": "Ei ole sobilik taastevõti",
"Ask your Riot admin to check <a>your config</a> for incorrect or duplicate entries.": "Palu, et sinu Riot'u haldur kontrolliks <a>sinu seadistusi</a> võimalike vigaste või topeltkirjete osas.",
"Cannot reach identity server": "Isikutuvastusserverit ei õnnestu leida",
"You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Sa võid registreeruda, kuid mõned funktsionaalsused pole kasutatavad seni, kuni isikutuvastusserver pole uuesti võrgus. Kui see teade tekib järjepanu, siis palun kontrolli oma seadistusi või võta ühendust serveri haldajaga.",
"You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Sa võid salasõna lähtestada, kuid mõned funktsionaalsused pole kasutatavad seni, kuni isikutuvastusserver pole uuesti võrgus. Kui see teade tekib järjepanu, siis palun kontrolli oma seadistusi või võta ühendust serveri haldajaga.",
"You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Sa võid sisse logida, kuid mõned funktsionaalsused pole kasutatavad seni, kuni isikutuvastusserver pole uuesti võrgus. Kui see teade tekib järjepanu, siis palun kontrolli oma seadistusi või võta ühendust serveri haldajaga.",
"No homeserver URL provided": "Koduserveri aadress on puudu",
"Unexpected error resolving homeserver configuration": "Koduserveri seadistustest selguse saamisel tekkis ootamatu viga",
"Unexpected error resolving identity server configuration": "Isikutuvastusserveri seadistustest selguse saamisel tekkis ootamatu viga",
"The message you are trying to send is too large.": "Sõnum, mida sa proovid saata, on liiga suur.",
"This homeserver has exceeded one of its resource limits.": "See koduserver ületanud ühe oma ressursipiirangutest.",
"Please <a>contact your service administrator</a> to continue using the service.": "Jätkamaks selle teenuse kasutamist, palun <a>võta ühendust oma serveri haldajaga</a>.",
"Unable to connect to Homeserver. Retrying...": "Ei saa ühendust koduserveriga. Proovin uuesti...",
"%(items)s and %(count)s others|other": "%(items)s ja %(count)s muud",
"%(items)s and %(count)s others|one": "%(items)s ja üks muu",
"%(items)s and %(lastItem)s": "%(items)s ja %(lastItem)s",
"a few seconds ago": "mõni sekund tagasi",
"%(num)s minutes ago": "%(num)s minutit tagasi",
"a few seconds from now": "mõne sekundi pärast",
"%(num)s minutes from now": "%(num)s minuti pärast",
"Use a few words, avoid common phrases": "Kasuta paari sõna, kuid väldi levinud fraase",
"No need for symbols, digits, or uppercase letters": "Sa ei pea sisestama erilisi tähemärke, numbreid ega suurtähti",
"Use a longer keyboard pattern with more turns": "Kasuta pikemaid klahvikombinatsioone, kus vajutatud klahvid pole kõrvuti ega kohakuti",
"Avoid repeated words and characters": "Väldi korduvaid sõnu ja tähemärke",
"Avoid sequences": "Väldi korduvaid klahviseeriaid",
"Avoid recent years": "Väldi hiljutisi aastaid",
"Avoid years that are associated with you": "Väldi aastaid, mida saaks sinuga seostada",
"Avoid dates and years that are associated with you": "Väldi kuupäevi ja aastaid, mida saaks sinuga seostada",
"Capitalization doesn't help very much": "Suurtähtede kasutamisest pole suurt kasu",
"All-uppercase is almost as easy to guess as all-lowercase": "Läbiva suurtähega kirjutatud teksti on sisuliselt sama lihte ära arvata, kui läbiva väiketähega kirjutatud teksti",
"Reversed words aren't much harder to guess": "Tagurpidi kirjutatud sõnu pole eriti keeruline ära arvata",
"Predictable substitutions like '@' instead of 'a' don't help very much": "Ennustatavatest asendustest nagu '@' 'a' asemel pole eriti kasu",
"Add another word or two. Uncommon words are better.": "Lisa veel mõni sõna. Ebatavaliste sõnade kasutamine on hea mõte.",
"Repeats like \"aaa\" are easy to guess": "Kordusi, nagu \"aaa\" on lihtne ära arvata",
"Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Kordusi, nagu \"abcabcabc\" on vaid natuke raskem ära arvata kui \"abc\"",
"Sequences like abc or 6543 are easy to guess": "Jadasid nagu \"abc\" või \"6543\" on lihtne ära arvata",
"Recent years are easy to guess": "Hiljutisi aastaid on lihtne ära arvata",
"Dates are often easy to guess": "Kuupäevi on sageli lihtne ära arvata",
"A word by itself is easy to guess": "Üksikut sõna on lihtne ära arvata",
"Names and surnames by themselves are easy to guess": "Nimesid ja perenimesid on lihtne ära arvata",
"Common names and surnames are easy to guess": "Üldisi nimesid ja perenimesid on lihtne ära arvata",
"Straight rows of keys are easy to guess": "Klaviatuuril järjest paiknevaid klahvikombinatsioone on lihtne ära arvata",
"Help us improve Riot": "Aidake meil täiustada Riot'it",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Saada meile <UsageDataLink>anonüümset kasutusteavet</UsageDataLink>, mis võimaldab meil Riot'it täiustada. Selline teave põhineb <PolicyLink>küpsiste kasutamisel</PolicyLink>.",
"I want to help": "Ma soovin aidata",
"No": "Ei",
"Restart": "Käivita uuesti",
"Upgrade your Riot": "Uuenda oma Riot'it",
"A new version of Riot is available!": "Uus Riot'i versioon on saadaval!",
"You: %(message)s": "Sina: %(message)s",
"There was an error joining the room": "Jututoaga liitumisel tekkis viga",
"Sorry, your homeserver is too old to participate in this room.": "Vabandust, sinu koduserver on siin jututoas osalemiseks liiga vana.",
"Please contact your homeserver administrator.": "Palun võta ühendust koduserveri haldajaga.",
"Failed to join room": "Jututoaga liitumine ei õnnestunud",
"Font scaling": "Fontide skaleerimine",
"Custom user status messages": "Kasutajate kohandatud olekuteated",
"Use IRC layout": "Kasuta IRC-tüüpi paigutust",
"Font size": "Fontide suurus",
"Custom font size": "Fontide kohandatud suurus",
"Enable automatic language detection for syntax highlighting": "Kasuta süntaksi esiletõstmisel automaatset keeletuvastust",
"Cross-signing private keys:": "Privaatvõtmed risttunnustamise jaoks:",
"Identity Server URL must be HTTPS": "Isikutuvastusserveri URL peab kasutama HTTPS-protokolli",
"Not a valid Identity Server (status code %(code)s)": "See ei ole sobilik isikutuvastusserver (staatuskood %(code)s)",
"Could not connect to Identity Server": "Ei saanud ühendust isikutuvastusserveriga",
"Checking server": "Kontrollin serverit",
"Change identity server": "Muuda isikutuvastusserverit",
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Kas katkestame ühenduse <current /> isikutuvastusserveriga ning selle asemel loome uue ühenduse serveriga <new />?",
"Terms of service not accepted or the identity server is invalid.": "Kas puudub nõustumine kasutustingimustega või on isikutuvastusserver vale.",
"The identity server you have chosen does not have any terms of service.": "Sinu valitud isikutuvastusserveril pole kasutustingimusi.",
"Disconnect identity server": "Katkesta ühendus isikutuvastusserveriga",
"Disconnect from the identity server <idserver />?": "Kas katkestame ühenduse isikutuvastusserveriga <idserver />?",
"Disconnect": "Katkesta ühendus",
"You should:": "Sa peaksid:",
"check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "kontrollima kas mõni brauseriplugin takistab ühendust isikutuvastusserveriga (nagu näiteks Privacy Badger)",
"contact the administrators of identity server <idserver />": "võtma ühendust isikutuvastusserveri <idserver /> haldajaga",
"wait and try again later": "ootama ja proovima hiljem uuesti",
"Disconnect anyway": "Ikkagi katkesta ühendus",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Sa jätkuvalt <b>jagad oma isikuandmeid</b> isikutuvastusserveriga <idserver />.",
"Go back": "Mine tagasi",
"Identity Server (%(server)s)": "Isikutuvastusserver %(server)s",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Sinu serveri haldur on lülitanud läbiva krüptimise omavahelistes jututubades ja otsesõnumites välja.",
"This room has been replaced and is no longer active.": "See jututuba on asendatud teise jututoaga ning ei ole enam kasutusel.",
"You do not have permission to post to this room": "Sul ei ole õigusi siia jututuppa kirjutamiseks",
"Bold": "Paks kiri",
"Italics": "Kaldkiri",
"Strikethrough": "Läbikriipsutus",
"Code block": "Koodiplokk",
"Show": "Näita",
"Message preview": "Sõnumi eelvaade",
"List options": "Loendi valikud",
"Show %(count)s more|other": "Näita veel %(count)s sõnumit",
"Show %(count)s more|one": "Näita veel %(count)s sõnumit",
"This room is private, and can only be joined by invitation.": "See jututuba on vaid omavaheliseks kasutuseks ning liitumine toimub kutse alusel.",
"Upgrade this room to version %(version)s": "Uuenda jututuba versioonini %(version)s",
"Upgrade Room Version": "Uuenda jututoa versioon",
"Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "Selle jututoa uuendamine eeldab tema praeguse ilmingu tegevuse lõpetamist ja uue jututoa loomist selle asemele. Selleks, et kõik kulgeks jututoas osalejate jaoks ladusalt, toimime nüüd nii:",
"Create a new room with the same name, description and avatar": "loome uue samanimelise jututoa, millel on sama kirjeldus ja tunnuspilt",
"Update any local room aliases to point to the new room": "uuendame kõik jututoa aliased nii, et nad viitaks uuele jututoale",
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "ei võimalda kasutajatel enam vanas jututoas suhelda ning avaldame seal teate, mis soovitab kõigil kolida uude jututuppa",
"Put a link back to the old room at the start of the new room so people can see old messages": "selleks et saaks vanu sõnumeid lugeda, paneme uue jututoa algusesse viite vanale jututoale",
"Automatically invite users": "Kutsu kasutajad automaatselt",
"Upgrade private room": "Uuenda omavaheline jututuba",
"Upgrade public room": "Uuenda avalik jututuba",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Jututoa uuendamine on keerukas toiming ning tavaliselt soovitatakse seda teha vaid siis, kui jututuba on vigade tõttu halvasti kasutatav, sealt on puudu vajalikke funktsionaalsusi või seal ilmneb turvavigu.",
"This usually only affects how the room is processed on the server. If you're having problems with your Riot, please <a>report a bug</a>.": "Selline tegevus mõjutab tavaliselt vaid viisi, kuidas jututoa andmeid töödeldakse serveris. Kui sinu kasutatavas Riot'is tekib vigu, siis palun saada meile <a>veateade</a>.",
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "Sa uuendad jututoa versioonist <oldVersion /> versioonini <newVersion />.",
"Clear Storage and Sign Out": "Tühjenda andmeruum ja logi välja",
"Send Logs": "Saada logikirjed",
"Refresh": "Värskenda",
"Unable to restore session": "Sessiooni taastamine ei õnnestunud",
"We encountered an error trying to restore your previous session.": "Meil tekkis eelmise sessiooni taastamisel viga.",
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Kui sa varem oled kasutanud uuemat Riot'i versiooni, siis sinu pragune sessioon ei pruugi olla sellega ühilduv. Sulge see aken ja jätka selle uuema versiooni kasutamist.",
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Brauseri andmeruumi tühjendamine võib selle vea lahendada, kui samas logid sa ka välja ning kogu krüptitud vestlusajalugu muutub loetamatuks.",
"Verification Pending": "Verifikatsioon on ootel"
}

View file

@ -2526,5 +2526,7 @@
"Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login youll need it to unlock your data.": "Créez une clé de récupération pour stocker vos clé et secrets de chiffrement avec les données de votre compte. Si vous navez plus accès à cette connexion, vous en aurez besoin pour déverrouiller vos données.",
"Create a Recovery Key": "Créer une clé de récupération",
"Upgrade your Recovery Key": "Mettre à jour votre clé de récupération",
"Store your Recovery Key": "Stocker votre clé de récupération"
"Store your Recovery Key": "Stocker votre clé de récupération",
"Use the improved room list (in development - will refresh to apply changes)": "Utiliser la liste de salons améliorée (en développement actualisera pour appliquer les changements)",
"Use the improved room list (will refresh to apply changes)": "Utiliser la liste de salons améliorée (actualisera pour appliquer les changements)"
}

View file

@ -1716,7 +1716,7 @@
"Sort by": "Orde por",
"Activity": "Actividade",
"A-Z": "A-Z",
"Unread rooms": "",
"Unread rooms": "Salas non lidas",
"Always show first": "Mostrar sempre primeiro",
"Show": "Mostrar",
"Message preview": "Vista previa da mensaxe",
@ -1780,5 +1780,187 @@
"Yours, or the other users session": "A túa, ou a sesión da outra usuaria",
"Trusted": "Confiable",
"Not trusted": "Non confiable",
"%(count)s verified sessions|other": "%(count)s sesións verificadas"
"%(count)s verified sessions|other": "%(count)s sesións verificadas",
"Use the improved room list (in development - will refresh to apply changes)": "Usar a lista de salas mellorada (desenvolvemento - actualizar para aplicar)",
"%(count)s verified sessions|one": "1 sesión verificada",
"Hide verified sessions": "Agochar sesións verificadas",
"%(count)s sessions|other": "%(count)s sesións",
"%(count)s sessions|one": "%(count)s sesión",
"Hide sessions": "Agochar sesións",
"Direct message": "Mensaxe directa",
"No recent messages by %(user)s found": "Non se atoparon mensaxes recentes de %(user)s",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Desprázate na cronoloxía para ver se hai algúns máis recentes.",
"Remove recent messages by %(user)s": "Eliminar mensaxes recentes de %(user)s",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Vas eliminar %(count)s mensaxes de %(user)s. Esto non ten volta, ¿desexas continuar?",
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Vas eliminar unha mensaxe de %(user)s. Esto non ten volta, ¿desexas continuar?",
"For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Podería demorar un tempo se é un número grande de mensaxes. Non actualices o cliente mentras tanto.",
"Remove %(count)s messages|other": "Eliminar %(count)s mensaxes",
"Remove %(count)s messages|one": "Eliminar 1 mensaxe",
"Remove recent messages": "Eliminar mensaxes recentes",
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> en %(roomName)s",
"Deactivate user?": "¿Desactivar usuaria?",
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Ao desactivar esta usuaria ficará desconectada e non poderá voltar a conectar. Ademáis deixará todas as salas nas que estivese. Esta acción non ten volta, ¿desexas desactivar esta usuaria?",
"Deactivate user": "Desactivar usuaria",
"Failed to deactivate user": "Fallo ao desactivar a usuaria",
"This client does not support end-to-end encryption.": "Este cliente non soporta o cifrado extremo-a-extremo.",
"Security": "Seguridade",
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.": "A sesión que intentas verificar non soporta a verificación por código QR ou por emoticonas, que é o que soporta Riot. Inténtao cun cliente diferente.",
"Verify by scanning": "Verificar escaneando",
"Ask %(displayName)s to scan your code:": "Pídelle a %(displayName)s que escanee o teu código:",
"If you can't scan the code above, verify by comparing unique emoji.": "Se non podes escanear o código superior, verifica comparando as emoticonas.",
"Verify by comparing unique emoji.": "Verficación por comparación de emoticonas.",
"Verify by emoji": "Verificar por emoticonas",
"Almost there! Is your other session showing the same shield?": "Case feito! ¿Podes ver as mesmas na túa outra sesión?",
"Almost there! Is %(displayName)s showing the same shield?": "Case feito! ¿está %(displayName)s mostrando as mesmas emoticonas?",
"Yes": "Si",
"Verify all users in a room to ensure it's secure.": "Verificar todas as usuarias da sala para asegurar que é segura.",
"Use the improved room list (will refresh to apply changes)": "Usa a lista de salas mellorada (actualizará para aplicar)",
"Strikethrough": "Sobrescrito",
"In encrypted rooms, verify all users to ensure its secure.": "En salas cifradas, verifica todas as usuarias para asegurar que é segura.",
"You've successfully verified your device!": "Verificaches correctamente o teu dispositivo!",
"You've successfully verified %(deviceName)s (%(deviceId)s)!": "Verificaches correctamente %(deviceName)s (%(deviceId)s)!",
"You've successfully verified %(displayName)s!": "Verificaches correctamente a %(displayName)s!",
"Verified": "Verficiado",
"Got it": "Vale",
"Start verification again from the notification.": "Inicia o proceso de novo desde a notificación.",
"Start verification again from their profile.": "Inicia a verificación outra vez desde o seu perfil.",
"Verification timed out.": "Verificación caducada.",
"You cancelled verification on your other session.": "Cancelaches a verificación na túa outra sesión.",
"%(displayName)s cancelled verification.": "%(displayName)s cancelou a verificación.",
"You cancelled verification.": "Cancelaches a verificación.",
"Verification cancelled": "Verificación cancelada",
"Compare emoji": "Comparar emoticonas",
"Encryption enabled": "Cifrado activado",
"Encryption not enabled": "Cifrado desactivado",
"The encryption used by this room isn't supported.": "O cifrado desta sala non está soportado.",
"React": "Reacciona",
"Message Actions": "Accións da mensaxe",
"Show image": "Mostrar imaxe",
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "Estás a ignorar a esta usuaria, polo que a imaxe está agochada. <a>Mostrar igualmente.</a>",
"You verified %(name)s": "Verificaches a %(name)s",
"You cancelled verifying %(name)s": "Cancelaches a verificación de %(name)s",
"%(name)s cancelled verifying": "%(name)s cancelou a verificación",
"You accepted": "Aceptaches",
"%(name)s accepted": "%(name)s aceptou",
"You declined": "Declinaches",
"You cancelled": "Cancelaches",
"%(name)s declined": "%(name)s declinou",
"%(name)s cancelled": "%(name)s cancelou",
"Accepting …": "Aceptando…",
"Declining …": "Declinando…",
"%(name)s wants to verify": "%(name)s desexa verificar",
"You sent a verification request": "Enviaches unha solicitude de verificación",
"Show all": "Mostrar todo",
"Reactions": "Reaccións",
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reaccionaron con %(content)s</reactedWith>",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reaccionaron con %(shortName)s</reactedWith>",
"Message deleted": "Mensaxe eliminada",
"Message deleted by %(name)s": "Mensaxe eliminada por %(name)s",
"This room is a continuation of another conversation.": "Esta sala é continuación doutra conversa.",
"Click here to see older messages.": "Preme aquí para ver mensaxes antigas.",
"Edited at %(date)s. Click to view edits.": "Editada o %(date)s. Preme para ver edicións.",
"edited": "editada",
"Can't load this message": "Non se cargou a mensaxe",
"Submit logs": "Enviar rexistro",
"Failed to load group members": "Fallou a carga dos membros do grupo",
"Frequently Used": "Utilizado con frecuencia",
"Smileys & People": "Sorrisos e Persoas",
"Animals & Nature": "Animais e Natureza",
"Food & Drink": "Comida e Bebida",
"Activities": "Actividades",
"Travel & Places": "Viaxes e Lugares",
"Objects": "Obxectos",
"Symbols": "Símbolos",
"Flags": "Bandeiras",
"Categories": "Categorías",
"Quick Reactions": "Reaccións rápidas",
"Cancel search": "Cancelar busca",
"Any of the following data may be shared:": "Calquera do seguinte podería ser compartido:",
"Your display name": "Nome mostrado",
"Your avatar URL": "URL do avatar",
"Your user ID": "ID de usuaria",
"Riot URL": "URL Riot",
"Room ID": "ID da sala",
"Widget ID": "ID do widget",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Ao utilizar este widget poderías compartir datos <helpIcon /> con %(widgetDomain)s e o teu Xestor de integracións.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Ao utilizar este widget poderías compartir datos <helpIcon /> con %(widgetDomain)s.",
"Widgets do not use message encryption.": "Os Widgets non usan cifrado de mensaxes.",
"Widget added by": "Widget engadido por",
"This widget may use cookies.": "Este widget podería usar cookies.",
"Maximize apps": "Maximizar apps",
"More options": "Máis opcións",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Por favor <newIssueLink>abre un novo informe</newIssueLink> en GitHub para poder investigar o problema.",
"Rotate Left": "Rotar á esquerda",
"Rotate counter-clockwise": "Rotar sentido contra-horario",
"Rotate Right": "Rotar á dereita",
"Rotate clockwise": "Rotar sentido horario",
"Language Dropdown": "Selector de idioma",
"%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s non fixeron cambios %(count)s veces",
"%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s non fixeron cambios",
"%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s non fixo cambios %(count)s veces",
"%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s non fixo cambios",
"QR Code": "Código QR",
"Room address": "Enderezo da sala",
"e.g. my-room": "ex. a-miña-sala",
"Some characters not allowed": "Algúns caracteres non permitidos",
"Please provide a room address": "Proporciona un enderezo para a sala",
"This address is available to use": "Este enderezo está dispoñible",
"This address is already in use": "Este enderezo xa se está a utilizar",
"Enter a server name": "Escribe un nome de servidor",
"Looks good": "Pinta ben",
"Can't find this server or its room list": "Non se atopa o servidor ou a súa lista de salas",
"All rooms": "Todas as salas",
"Your server": "O teu servidor",
"Are you sure you want to remove <b>%(serverName)s</b>": "¿Tes a certeza de querer eliminar <b>%(serverName)s</b>",
"Remove server": "Eliminar servidor",
"Matrix": "Matrix",
"Add a new server": "Engadir novo servidor",
"Server name": "Nome do servidor",
"Add a new server...": "Engadir un novo servidor...",
"%(networkName)s rooms": "Salas de %(networkName)s",
"That doesn't look like a valid email address": "Non semella un enderezo de email válido",
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Usa un servidor de identidade para convidar por email. <default>Usa valor por omisión (%(defaultIdentityServerName)s)</default> ou xestionao en <settings>Axustes</settings>.",
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Usa un servidor de identidade para convidar por email. Xestionao en <settings>Axustes</settings>.",
"The following users may not exist": "As seguintes usuarias poderían non existir",
"Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Non se atopou o perfil dos IDs Matrix da lista inferior - ¿Desexas convidalas igualmente?",
"Invite anyway and never warn me again": "Convidar igualmente e non avisarme outra vez",
"Invite anyway": "Convidar igualmente",
"Close dialog": "Pechar diálogo",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Cóntanos o que fallou ou, mellor aínda, abre un informe en GitHub que describa o problema.",
"Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Lembra: o teu navegador non está soportado, polo que poderían acontecer situacións non agardadas.",
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Antes de enviar os rexistros, deberías <a>abrir un informe en GitHub</a> para describir o problema.",
"GitHub issue": "Informe en GitHub",
"Notes": "Notas",
"Unable to load commit detail: %(msg)s": "Non se cargou o detalle do commit: %(msg)s",
"Removing…": "Eliminando…",
"Destroy cross-signing keys?": "Destruír chaves de sinatura-cruzada?",
"Clear cross-signing keys": "Baleirar chaves de sinatura-cruzada",
"Clear all data in this session?": "¿Baleirar todos os datos desta sesión?",
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "O baleirado dos datos da sesión é permanente. As mensaxes cifradas perderánse a menos que as súas chaves estiveren nunha copia de apoio.",
"Clear all data": "Eliminar todos os datos",
"Please enter a name for the room": "Escribe un nome para a sala",
"Set a room address to easily share your room with other people.": "Establece un enderezo para a sala fácil de compartir con outras persoas.",
"This room is private, and can only be joined by invitation.": "Esta sala é privada, e só te podes unir a través dun convite.",
"You cant disable this later. Bridges & most bots wont work yet.": "Podes desactivar esto posteriormente. As pontes e maioría de bots aínda non funcionarán.",
"Enable end-to-end encryption": "Activar cifrado extremo-a-extremo",
"Create a public room": "Crear sala pública",
"Create a private room": "Crear sala privada",
"Topic (optional)": "Asunto (optativo)",
"Make this room public": "Facer pública esta sala",
"Hide advanced": "Ocultar Avanzado",
"Show advanced": "Mostrar Avanzado",
"Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Evitar que usuarias de outros servidores matrix se unan a esta sala (Este axuste non se pode cambiar máis tarde!)",
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Para evitar perder o historial da conversa, debes exportar as chaves da sala antes de desconectarte. Necesitarás voltar á nova versión de Riot para facer esto",
"You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Xa utilizaches unha versión máis nova de Riot nesta sesión. Para usar esta versión novamente con cifrado extremo-a-extremo tes que desconectarte e voltar a conectar.",
"Incompatible Database": "Base de datos non compatible",
"Continue With Encryption Disabled": "Continuar con Cifrado Desactivado",
"Are you sure you want to deactivate your account? This is irreversible.": "¿Tes a certeza de querer desactivar a túa conta? Esto é irreversible.",
"Confirm account deactivation": "Confirma a desactivación da conta",
"There was a problem communicating with the server. Please try again.": "Houbo un problema ao comunicar co servidor. Inténtao outra vez.",
"Server did not require any authentication": "O servidor non require auténticación",
"Server did not return valid authentication information.": "O servidor non devolveu información válida de autenticación.",
"View Servers in Room": "Ver Servidores na Sala",
"Verification Requests": "Solicitudes de Verificación",
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verifica esta usuaria para marcala como confiable. Ao confiar nas usuarias proporcionache tranquilidade extra cando usas cifrado de extremo-a-extremo.",
"Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Ao verificar esta usuaria marcarás a súa sesión como confiable, e tamén marcará a túa sesión como confiable para elas."
}

View file

@ -2514,5 +2514,7 @@
"Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login youll need it to unlock your data.": "Készíts Visszaállítási kulcsot, hogy a fiók adatokkal tárolhasd a titkosítási kulcsokat és jelszavakat. Szükséged lesz rá hogy hozzáférj az adataidhoz ha elveszted a hozzáférésed ehhez a bejelentkezéshez.",
"Create a Recovery Key": "Visszaállítási kulcs készítése",
"Upgrade your Recovery Key": "A Visszaállítási kulcs fejlesztése",
"Store your Recovery Key": "Visszaállítási kulcs tárolása"
"Store your Recovery Key": "Visszaállítási kulcs tárolása",
"Use the improved room list (in development - will refresh to apply changes)": "Használd a fejlesztett szoba listát (fejlesztés alatt - a változások a frissítés után aktiválódnak)",
"Use the improved room list (will refresh to apply changes)": "Használd a fejlesztett szoba listát (a változások életbe lépéséhez újra fog tölteni)"
}

View file

@ -2512,5 +2512,15 @@
"This isn't a valid recovery key": "Questa non è una chiave di ripristino valida",
"Looks good!": "Sembra giusta!",
"Use Recovery Key or Passphrase": "Usa la chiave o password di recupero",
"Use Recovery Key": "Usa chiave di recupero"
"Use Recovery Key": "Usa chiave di recupero",
"Use the improved room list (in development - will refresh to apply changes)": "Usa l'elenco di stanze migliorato (in sviluppo - verrà ricaricato per applicare le modifiche)",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "Inserisci la tua chiave di recupero o una <a>password di recupero</a> per continuare.",
"Enter your Recovery Key to continue.": "Inserisci la tua chiave di recupero per continuare.",
"Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Aggiorna la tua chiave di recupero per memorizzare le chiavi di cifratura e i segreti con i dati del tuo account. Se perdi l'accesso a questo login, ti servirà per sbloccare i tuoi dati.",
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Conserva la tua chiave di recupero in un posto sicuro, può essere usata per sbloccare i tuoi messaggi e dati cifrati.",
"Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login youll need it to unlock your data.": "Crea un chiave di recupero per memorizzare le chiavi di cifratura e i segreti con i dati del tuo account. Se perdi l'accesso a questo login, ti servirà per sbloccare i tuoi dati.",
"Create a Recovery Key": "Crea una chiave di recupero",
"Upgrade your Recovery Key": "Aggiorna la chiave di recupero",
"Store your Recovery Key": "Salva la chiave di recupero",
"Use the improved room list (will refresh to apply changes)": "Usa l'elenco stanze migliorato (verrà ricaricato per applicare le modifiche)"
}

View file

@ -1400,5 +1400,36 @@
"Service": "サービス",
"Summary": "概要",
"Document": "ドキュメント",
"Appearance": "外観"
"Appearance": "外観",
"Other users may not trust it": "他のユーザーはこのセッションを信頼しない可能性があります",
"Show a placeholder for removed messages": "削除されたメッセージの場所にプレースホルダーを表示",
"Show join/leave messages (invites/kicks/bans unaffected)": "参加/退出のメッセージを表示 (招待/追放/ブロック には影響しません)",
"Prompt before sending invites to potentially invalid matrix IDs": "不正な可能性のある Matrix ID に招待を送るまえに確認を表示",
"Order rooms by name": "名前順で部屋を整列",
"Show rooms with unread notifications first": "未読通知のある部屋をトップに表示",
"Show shortcuts to recently viewed rooms above the room list": "最近表示した部屋のショートカットを部屋リストの上に表示",
"Show previews/thumbnails for images": "画像のプレビュー/サムネイルを表示",
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "あなたのアカウントではクロス署名の認証情報がシークレットストレージに保存されていますが、このセッションでは信頼されていません。",
"Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.": "Riot はウェブブラウザでは暗号化されたメッセージを安全にキャッシュできません。暗号化されたメッセージを検索結果に表示するには <riotLink>Riot Desktop</riotLink> を利用してください。",
"This session is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "このセッションでは<b>キーをバックアップしていません</b>。ですが、あなたは復元に使用したり今後キーを追加したりできるバックアップを持っています。",
"Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "このセッションのみにあるキーを失なわないために、セッションをキーバックアップに接続しましょう。",
"Connect this session to Key Backup": "このセッションをキーバックアップに接続",
"Autocomplete delay (ms)": "自動補完の遅延 (ms)",
"Missing media permissions, click the button below to request.": "メディア権限が不足しています、リクエストするには下のボタンを押してください。",
"Request media permissions": "メディア権限をリクエスト",
"Joining room …": "部屋に参加中...",
"Join the discussion": "話し合いに参加",
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s はプレビューできません。部屋に参加しますか?",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "他のユーザーがあなたのホームサーバー (%(localDomain)s) を通じてこの部屋を見つけられるよう、アドレスを設定しましょう",
"<b>Warning</b>: You should only do this on a trusted computer.": "<b>警告</b>: 信頼できるコンピュータでのみこれを実行するべきです。",
"Enter recovery key": "リカバリキーを入力",
"Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.": "暗号化されたメッセージへアクセスしたりクロス署名認証情報で他のセッションを承認するにはリカバリキーを入力してください。",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>.": "リカバリキーを忘れた場合は<button>新しいリカバリオプションをセットアップ</button>できます。",
"Verify this login": "このログインを承認",
"Signing In...": "サインインしています...",
"If you've joined lots of rooms, this might take a while": "たくさんの部屋に参加している場合は、時間がかかる可能性があります",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "このログインを他のセッションで承認し、あなたの認証情報を確認すれば、暗号化されたメッセージへアクセスできるようになります。",
"This requires the latest Riot on your other devices:": "最新版の Riot が他のあなたのデバイスで実行されている必要があります:",
"or another cross-signing capable Matrix client": "もしくはクロス署名に対応した他の Matrix クライアント",
"Without completing security on this session, it wont have access to encrypted messages.": "このセッションでのセキュリティを完了させない限り、暗号化されたメッセージにはアクセスできません。"
}

350
src/i18n/strings/kab.json Normal file
View file

@ -0,0 +1,350 @@
{
"Confirm": "Sentem",
"Analytics": "Tiselḍin",
"Error": "Tuccḍa",
"Dismiss": "Agwi",
"OK": "IH",
"Permission Required": "Tasiregt tlaq",
"Continue": "Kemmel",
"Cancel": "Semmet",
"Sun": "Ace",
"Mon": "Ari",
"Tue": "Ara",
"Wed": "Aha",
"Thu": "Amh",
"Fri": "Sem",
"Sat": "Sed",
"Jan": "Yen",
"Feb": "Fuṛ",
"Mar": "Meɣ",
"Apr": "Yeb",
"May": "May",
"Jun": "Yun",
"Jul": "Yul",
"Aug": "Ɣuc",
"Sep": "Cte",
"Oct": "Tub",
"Nov": "Wam",
"Dec": "Duj",
"PM": "MD",
"AM": "FT",
"Trust": "Ittkel",
"Create Account": "Rnu amiḍan",
"Sign In": "Kcem",
"Default": "Amezwer",
"Moderator": "Aseɣyad",
"Admin": "Anedbal",
"You need to be logged in.": "Tesriḍ ad teqqneḍ.",
"Messages": "Iznan",
"Actions": "Tigawin",
"Advanced": "Talqayt",
"Other": "Nniḍen",
"Usage": "Aseqdec",
"Thank you!": "Tanemmirt!",
"Reason": "Taɣẓint",
"Someone": "Albaɛḍ",
"Light": "Aceɛlal",
"Dark": "Aberkan",
"Done": "Immed",
"Add another word or two. Uncommon words are better.": "Rnu awal-nniḍen neɣ sin. Awalen imexḍa ad lhun.",
"No": "Uhu",
"Review": "Senqed",
"Later": "Ticki",
"Notifications": "Ilɣa",
"Close": "Mdel",
"Warning": "Asmigel",
"Ok": "Ih",
"Set password": "Sbadu awal uffir",
"Upgrade": "Leqqem",
"Verify": "Senqed",
"What's New": "D acu-t umaynut",
"Update": "Leqqem",
"Restart": "Ales tanekra",
"Font size": "Tuɣzi n tsefsit",
"Custom font size": "Tiddi n tsefsit yugnen",
"Decline": "Agwi",
"Accept": "Qbel",
"or": "neɣ",
"Start": "Bdu",
"Cat": "Amcic",
"Lion": "Izem",
"Rabbit": "Awtul",
"Turtle": "Afekrun",
"Tree": "Aseklu",
"Clock": "Tamrint",
"Book": "Adlis",
"Lock": "Sekkeṛ",
"Key": "Tasarut",
"Telephone": "Tiliγri",
"Flag": "Anay",
"Bicycle": "Azlalam",
"Ball": "Balles",
"Anchor": "Tamdeyt",
"Headphones": "Casque",
"Folder": "Akaram",
"Upload": "Sali",
"Remove": "Sfeḍ",
"Show less": "Sken-d drus",
"Show more": "Sken-d ugar",
"Warning!": "Ɣur-k!",
"Current password": "Awal uffir amiran",
"Password": "Awal uffir",
"New Password": "Awal uffir amaynut",
"Confirm password": "Sentem awal uffir",
"Change Password": "Snifel Awal Uffir",
"not found": "ulac-it",
"Authentication": "Asesteb",
"ID": "ID",
"Manage": "Sefrek",
"Enable": "Rmed",
"Keywords": "Awalen tisura",
"Clear notifications": "Sfeḍ ilɣuyen",
"Off": "Insa",
"Display Name": "Mefffer isem",
"Save": "Sekles",
"Disconnect": "Yeffeɣ",
"Go back": "Uɣal ɣer deffir",
"Change": "Changer",
"Theme": "Asentel",
"Success": "Yedda",
"Profile": "Amaɣnu",
"Account": "Amiḍan",
"General": "Amatu",
"Legal": "Usḍif",
"Credits": "Asenmer",
"Chat with Riot Bot": "Asqerdec akked Riot Bot",
"FAQ": "Isteqsiyen FAQ",
"Keyboard Shortcuts": "Inegzumen n unasiw",
"Versions": "Ileqman",
"None": "Ula yiwen",
"Unsubscribe": "Se désabonner",
"Ignore": "Ttu",
"Subscribe": "Jerred",
"Preferences": "Tiwelhiwin",
"Composer": "Editeur",
"Timeline": "Amazray",
"Microphone": "Asawaḍ",
"Camera": "Takamiṛatt",
"Sounds": "Imeslan",
"Reset": "Ales awennez",
"Browse": "Snirem",
"Default role": "Tamlilt tamzwert",
"Permissions": "Tisirag",
"Anyone": "Yal yiwen",
"Encryption": "Awgelhen",
"Complete": "Yemmed",
"Share": "Bḍu",
"Verification code": "Tangalt n usenqed",
"Add": "Rnu",
"Email Address": "Tansa imayl",
"Phone Number": "Uṭṭun n tiliɣri",
"Upload file": "Azen afaylu",
"Bold": "Azuran",
"Strikethrough": "Jerreḍ",
"Quote": "Citation",
"Loading...": "Yessalay-ed…",
"Idle": "Idle",
"Unknown": "D arussin",
"Settings": "Iɣewwaren",
"Search": "Nadi",
"Favourites": "Ismenyifen",
"People": "Imdanen",
"Sign Up": "Jerred",
"Reject": "Aggi",
"Not now": "Mačči tura",
"Sort by": "Smizzwer s",
"Activity": "Armud",
"A-Z": "A-Z",
"Show": "Sken",
"Options": "Tinefrunin",
"Server error": "Tuccḍa n uqeddac",
"Members": "Imedrawen",
"Files": "Ifuyla",
"Trusted": "De confiance",
"Invite": "Nced…",
"Unmute": "Susem",
"Mute": "Kkes imesli",
"Are you sure?": "Tebɣiḍ ?",
"Security": "Taɣellist",
"Yes": "Ih",
"Verified": "Verified",
"Got it": "Awi-t",
"Sunday": "Acer",
"Monday": "Arim",
"Tuesday": "Aram",
"Wednesday": "Ahad",
"Thursday": "Amhad",
"Friday": "Sem",
"Saturday": "Sed",
"Today": "Ass-a",
"Yesterday": "Iḍelli",
"View Source": "Sken aɣbalu",
"Reply": "Err",
"Edit": "Ẓreg",
"Attachment": "Attachement",
"Error decrypting attachment": "Tuccḍa deg uzmak n ufaylu yeddan",
"Show all": "Sken akk",
"Message deleted": "Izen yettwakkes",
"Copied!": "Yettusukken!",
"edited": "yeẓreg",
"Food & Drink": "Učči aked tissit",
"Objects": "Tiɣawsiwin",
"Symbols": "Izamulen",
"Flags": "Anayen",
"Categories": "Taggayin",
"More options": "Ugar n textirin",
"Join": "Semlil",
"No results": "Ulac igmad",
"collapse": "sneḍfes",
"Rotate counter-clockwise": "Zzi di tnila tanemgalt n tsegnatin n temrilt",
"Rotate clockwise": "Zzi di tnila n tsegnatin n temrilt",
"Add User": "Rnu aseqdac",
"Server name": "Isem n uqeddac",
"email address": "tansa imayl",
"Close dialog": "Mdel tanaka n usdiwen",
"Notes": "Tamawt",
"Unavailable": "Ulac-it",
"Changelog": "Aɣmis n ibeddilen",
"Removing…": "Tukksa…",
"Confirm Removal": "Serggeg tukksa",
"Example": "Amedya",
"example": "amedya",
"Create": "Snulfu-d",
"Name": "Isem",
"Sign out": "Ffeɣ",
"Back": "Retour",
"Send": "Azen",
"Suggestions": "Isumar",
"Go": "Ddu",
"Session name": "Nom de session",
"Send report": "Azen aneqqis",
"Refresh": "Sismeḍ",
"Email address": "Tansa email",
"Skip": "Zgel",
"Username not available": "Ulac isem n useqdac",
"Checking...": "Asenqed...",
"Username available": "Yella yisem n useqdac",
"Terms of Service": "Tiwtilin n useqdec",
"Service": "Ameẓlu",
"Summary": "Agzul",
"Document": "isemli",
"Next": "Ar zdat",
"Upload files": "Azen ifuyla",
"Appearance": "Udem",
"Allow": "Sireg",
"Deny": "Agwi",
"Custom": "Personnalisé",
"Source URL": "URL aγbalu",
"Notification settings": "Iɣewwaṛen n yilɣa",
"Leave": "Ffeɣ",
"Set status": "Sbadu addad",
"Hide": "Ffer",
"Home": "Agejdan",
"Sign in": "Qqen",
"Help": "Tallelt",
"Reload": "Smiren",
"powered by Matrix": "s lmendad n Matrix",
"Custom Server Options": "Iɣewwaren n uqeddac udmawan",
"Code": "Tangalt",
"Submit": "Azen",
"Email": "Imayl",
"Username": "Isem n useqdac",
"Phone": "Tiliɣri",
"Passwords don't match": "Awal uffiren ur menṭaḍen ara",
"Email (optional)": "Imayl (Afrayan)",
"Register": "Jerred",
"Free": "Ilelli",
"Failed to upload image": "Ur yezmir ad yessali tugna",
"Description": "Aseglem",
"Explore": "Snirem",
"Filter": "Imsizdeg",
"Explore rooms": "Snirem tixxamin",
"Unknown error": "Erreur inconnue",
"Logout": "Tufɣa",
"Preview": "Timeẓriwt",
"View": "Ɣeṛ",
"Guest": "Inebgi",
"Your profile": "Amaɣnu-ik",
"Feedback": "Tikti",
"Your password has been reset.": "Awal n uɛeddi inek yules awennez.",
"Syncing...": "Amtawi",
"Create account": "Rnu amiḍan",
"Create your account": "Rnu amiḍan-ik",
"Go Back": "Précédent",
"Commands": "Tiludna",
"Users": "Iseqdacen",
"Export": "Sifeḍ",
"Import": "Kter",
"Restore": "Err-d",
"Copy": "Nγel",
"Download": "Sider",
"Retry": "Ɛreḍ tikkelt nniḍen",
"Starting backup...": "Asenker n uḥraz...",
"Success!": "Akka d rrbeḥ !",
"Disable": "Désactiver",
"Navigation": "Tunigin",
"Calls": "Appels",
"Alt": "Alt",
"Shift": "Shift",
"New line": "Izirig amaynut",
"Upload a file": "Sali-d afaylu",
"Page Up": "Asebter afellay",
"Page Down": "Asebter adday",
"Esc": "Senser",
"Enter": "Anekcum",
"Space": "Tallunt",
"End": "Taggara",
"This email address is already in use": "Tansa-agi n yimayl tettuseqdac yakan",
"This phone number is already in use": "Uṭṭun-agi n tilifun yettuseqddac yakan",
"Your Riot is misconfigured": "Riot inek(inem) ur ittusbadu ara",
"Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Ma ulac aɣilif, sebded <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, neɣ<safariLink>Safari</safariLink> i tirmit igerrzen.",
"I understand the risks and wish to continue": "Gziɣ ayen ara d-yeḍrun maca bɣiɣ ad kemmleɣ",
"Use Single Sign On to continue": "Seqdec anekcum asuf akken ad tkemmleḍ",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Sentem timerna n tansa-a n yimayl s useqdec n unekcum asuf i ubeggen n timagit-in(im).",
"Single Sign On": "Anekcum asuf",
"Confirm adding email": "Sentem timerna n yimayl",
"Click the button below to confirm adding this email address.": "Sit ɣef tqeffalt yellan ddaw i usentem n tmerna n tansa-a n yimayl.",
"Add Email Address": "Rnu tansa n yimayl",
"Failed to verify email address: make sure you clicked the link in the email": "Asenqed n tansa n yimayl ur yeddi ara: wali ma yella tsateḍ ɣef useɣwen yellan deg yimayl",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Sentem timerna n wuṭṭun n tilifun s useqdec n unekcum asuf i ubeggen n timagit-ik(im).",
"Confirm adding phone number": "Sentem timerna n wuṭṭun n tilifun",
"Click the button below to confirm adding this phone number.": "Sit ɣef tqeffalt yellan ddaw i usentem n tmerna n wuṭṭun-a n tilifun.",
"Add Phone Number": "Rnu uṭṭun n tilifun",
"The platform you're on": "Tiɣerɣert ideg telliḍ akka tura",
"The version of Riot": "Lqem n Riot",
"Whether or not you're logged in (we don't record your username)": "Ma teqqneḍ neɣ uhu (isem-ik(im) n useqdac ur yettwasekles ara)",
"Your language of choice": "Tutlayt i tferneḍ",
"Which officially provided instance you are using, if any": "Tummant i d-yettunefken tunṣibt ara tesseqdace, ma yella tella",
"Your homeserver's URL": "URL n uqeddac-ik(im) agejdan",
"e.g. %(exampleValue)s": "e.g. %(exampleValue)s",
"Every page you use in the app": "Isebtar akk i tesseqdaceḍ deg usnas",
"e.g. <CurrentPageURL>": "e.g. <CurrentPageURL>",
"Your user agent": "Ameggi-ik(im) aseqdac",
"Your device resolution": "Afray n yiben-ik(im)",
"Jump to oldest unread message": "Uɣal alamma d izen aqdim ur nettwaɣra ara",
"Jump to room search": "Ɛeddi ɣer unadi n texxamt",
"Navigate up/down in the room list": "Inig s afellay/adday deg tebdert n texxamin",
"Select room from the room list": "Fren taxxamt seg tebdert n texxamin",
"Collapse room list section": "Fneẓ tigemi n tebdert n texxamin",
"Expand room list section": "Snerni tigezmi n tebdert n texxamin",
"Clear room list filter field": "Kkes urti n usizdeg n tebdert n texxamin",
"Previous/next unread room or DM": "Taxxamt neɣ izen uzrid ur yettwaɣra send/seld",
"Previous/next room or DM": "Taxxamt neɣ izen usrid send/seld",
"Toggle the top left menu": "Sken/ffer umuɣ aεlayan azelmaḍ",
"Close dialog or context menu": "Mdel umuɣ n udiwenni neɣ n ugbur",
"Activate selected button": "Rmed taqeffalt i d-yettwafernen",
"Toggle right panel": "Sken/ffer agalis ayeffus",
"Toggle this dialog": "Sken/ffer adiwanni-a",
"Move autocomplete selection up/down": "Err tafrant n tacart tawurmant afellay/adday",
"Cancel autocomplete": "Sefsex tacaṛt tawurmant",
"Toggle Bold": "Err-it d azuran",
"Toggle Italics": "Err-it ɣer uknan",
"Toggle Quote": "Err-it ɣer yizen aneẓli",
"Navigate recent messages to edit": "Nadi iznan imaynuten i usiẓreg",
"Jump to start/end of the composer": "Ṛuḥ ɣer tazwara/taggara n yimsuddes",
"Navigate composer history": "Nadi deg uzray n yimsuddes",
"Cancel replying to a message": "Sefsex tiririt ɣef yizen",
"Toggle microphone mute": "Rmed/sens tanusi n usawaḍ",
"Toggle video on/off": "Rmed/sens tavidyut",
"Scroll up/down in the timeline": "Drurem gar afellay/addday n tesnakudt"
}

View file

@ -1354,5 +1354,135 @@
"Jump to read receipt": "Hopp til lesekvitteringen",
"Mention": "Nevn",
"Community Name": "Samfunnets navn",
"Dismiss read marker and jump to bottom": "Avføy lesekvitteringen og hopp ned til bunnen"
"Dismiss read marker and jump to bottom": "Avføy lesekvitteringen og hopp ned til bunnen",
"If you cancel now, you won't complete verifying your other session.": "Hvis du avbryter nå, vil du ikke ha fullført verifiseringen av den andre økten din.",
"Room name or address": "Rommets navn eller adresse",
"sent an image.": "sendte et bilde.",
"Light": "Lys",
"Dark": "Mørk",
"Verify your other session using one of the options below.": "Verifiser den andre økten din med en av metodene nedenfor.",
"User %(user_id)s does not exist": "Brukeren %(user_id)s eksisterer ikke",
"Use a few words, avoid common phrases": "Bruk noen få ord, unngå vanlig fraser",
"I want to help": "Jeg vil hjelpe til",
"Ok": "OK",
"Set password": "Bestem passord",
"Encryption upgrade available": "Krypteringsoppdatering tilgjengelig",
"Verify the new login accessing your account: %(name)s": "Verifiser den nye påloggingen som vil ha tilgang til kontoen din: %(name)s",
"Restart": "Start på nytt",
"You: %(message)s": "Du: %(message)s",
"Font scaling": "Skrifttypeskalering",
"Use IRC layout": "Bruk IRC-oppsett",
"Font size": "Skriftstørrelse",
"Custom font size": "Tilpasset skriftstørrelse",
"You've successfully verified this user.": "Du har vellykket verifisert denne brukeren.",
"Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Venter på at den andre økten din, %(deviceName)s (%(deviceId)s), skal verifisere …",
"Waiting for your other session to verify…": "Venter på at den andre økten din skal verifisere …",
"Santa": "Julenisse",
"Delete %(count)s sessions|other": "Slett %(count)s økter",
"Delete %(count)s sessions|one": "Slett %(count)s økt",
"Backup version: ": "Sikkerhetskopiversjon: ",
"wait and try again later": "vent og prøv igjen senere",
"Size must be a number": "Størrelsen må være et nummer",
"Customise your appearance": "Tilpass utseendet du bruker",
"Labs": "Laboratoriet",
"eg: @bot:* or example.org": "f.eks.: @bot:* eller example.org",
"To link to this room, please add an address.": "For å lenke til dette rommet, vennligst legg til en adresse.",
"Remove %(phone)s?": "Vil du fjerne %(phone)s?",
"Online for %(duration)s": "På nett i %(duration)s",
"Favourites": "Favoritter",
"Create room": "Opprett et rom",
"People": "Folk",
"Sort by": "Sorter etter",
"Activity": "Aktivitet",
"A-Z": "A-Å",
"Unread rooms": "Uleste rom",
"Always show first": "Alltid vis først",
"Show": "Vis",
"Message preview": "Meldingsforhåndsvisning",
"Leave Room": "Forlat rommet",
"This room has no local addresses": "Dette rommet har ikke noen lokale adresser",
"Waiting for you to accept on your other session…": "Venter på at du aksepterer på den andre økten din …",
"Remove recent messages by %(user)s": "Fjern nylige meldinger fra %(user)s",
"Remove %(count)s messages|other": "Slett %(count)s meldinger",
"Remove %(count)s messages|one": "Slett 1 melding",
"Almost there! Is your other session showing the same shield?": "Nesten fremme! Viser den andre økten din det samme skjoldet?",
"You've successfully verified your device!": "Du har vellykket verifisert enheten din!",
"You've successfully verified %(deviceName)s (%(deviceId)s)!": "Du har vellykket verifisert %(deviceName)s (%(deviceId)s)!",
"You've successfully verified %(displayName)s!": "Du har vellykket verifisert %(displayName)s!",
"You cancelled verification on your other session.": "Du avbrøt verifiseringen på den andre økten din.",
"You sent a verification request": "Du sendte en verifiseringsforespørsel",
"Error decrypting video": "Feil under dekryptering av video",
"Message deleted": "Meldingen ble slettet",
"Message deleted by %(name)s": "Meldingen ble slettet av %(name)s",
"Click here to see older messages.": "Klikk for å se eldre meldinger.",
"Add an Integration": "Legg til en integrering",
"Can't load this message": "Klarte ikke å laste inn denne meldingen",
"Categories": "Kategorier",
"Popout widget": "Utsprettsmodul",
"QR Code": "QR-kode",
"Room address": "Rommets adresse",
"This address is available to use": "Denne adressen er allerede i bruk",
"ex. @bob:example.com": "f.eks. @bob:example.com",
"Failed to send logs: ": "Mislyktes i å sende loggbøker: ",
"Incompatible Database": "Inkompatibel database",
"Event Content": "Hendelsesinnhold",
"Verification Requests": "Verifiseringsforespørsler",
"Integrations are disabled": "Integreringer er skrudd av",
"Integrations not allowed": "Integreringer er ikke tillatt",
"Confirm to continue": "Bekreft for å fortsette",
"Clear cache and resync": "Tøm mellomlageret og synkroniser på nytt",
"Manually export keys": "Eksporter nøkler manuelt",
"Use this session to verify your new one, granting it access to encrypted messages:": "Bruk denne økten til å verifisere din nye økt, som vil gi den tilgang til krypterte meldinger:",
"If you didnt sign in to this session, your account may be compromised.": "Dersom det ikke var du som logget deg på økten, kan kontoen din ha blitt kompromittert.",
"Automatically invite users": "Inviter brukere automatisk",
"Verification Pending": "Avventer verifisering",
"Username invalid: %(errMessage)s": "Brukernavnet er ugyldig: %(errMessage)s",
"An error occurred: %(error_string)s": "En feil oppstod: %(error_string)s",
"Share Room": "Del rommet",
"Share User": "Del brukeren",
"Upload %(count)s other files|other": "Last opp %(count)s andre filer",
"Upload %(count)s other files|one": "Last opp %(count)s annen fil",
"Appearance": "Utseende",
"Verify other session": "Verifiser en annen økt",
"Keys restored": "Nøklene ble gjenopprettet",
"Address (optional)": "Adresse (valgfritt)",
"Reject invitation": "Avslå invitasjonen",
"Resend edit": "Send redigeringen på nytt",
"Resend removal": "Send slettingen på nytt",
"Start authentication": "Begynn autentisering",
"The email field must not be blank.": "E-postfeltet kan ikke stå tomt.",
"The username field must not be blank.": "Brukernavnfeltet kan ikke stå tomt.",
"The phone number field must not be blank.": "Telefonnummerfeltet kan ikke stå tomt.",
"The password field must not be blank.": "Passordfeltet kan ikke stå tomt.",
"Couldn't load page": "Klarte ikke å laste inn siden",
"Add to summary": "Legg til i oppsummeringen",
"Unable to accept invite": "Klarte ikke å akseptere invitasjonen",
"Unable to join community": "Klarte ikke å bli med i samfunnet",
"Unable to leave community": "Klarte ikke å forlate samfunnet",
"You are an administrator of this community": "Du er en administrator i dette samfunnet",
"You are a member of this community": "Du er et medlem av dette samfunnet",
"Failed to load %(groupId)s": "Klarte ikke å laste inn %(groupId)s",
"Liberate your communication": "Frigjør kommunikasjonen din",
"Explore Public Rooms": "Utforsk offentlige rom",
"Create a Group Chat": "Opprett en gruppechat",
"Review terms and conditions": "Gå gjennom betingelser og vilkår",
"delete the address.": "slette adressen.",
"%(count)s of your messages have not been sent.|one": "Meldingen din ble ikke sendt.",
"Jump to first invite.": "Hopp til den første invitasjonen.",
"You seem to be uploading files, are you sure you want to quit?": "Du ser til å laste opp filer, er du sikker på at du vil avslutte?",
"Switch to light mode": "Bytt til lys modus",
"Switch to dark mode": "Bytt til mørk modus",
"Switch theme": "Bytt tema",
"All settings": "Alle innstillinger",
"Archived rooms": "Arkiverte rom",
"Feedback": "Tilbakemelding",
"Account settings": "Kontoinnstillinger",
"Emoji Autocomplete": "Auto-fullfør emojier",
"Confirm encryption setup": "Bekreft krypteringsoppsett",
"Create key backup": "Opprett nøkkelsikkerhetskopi",
"Set up Secure Messages": "Sett opp sikre meldinger",
"Toggle Bold": "Veksle fet",
"Toggle Italics": "Veksle kursiv",
"Toggle Quote": "Veksle siteringsformat",
"Upload a file": "Last opp en fil"
}

View file

@ -52,7 +52,7 @@
"Invites user with given id to current room": "Приглашает пользователя с заданным ID в текущую комнату",
"Sign in with": "Войти с помощью",
"Joins room with given alias": "Входит в комнату с заданным псевдонимом",
"Kicks user with given id": "Выкидывает пользователя с заданным ID",
"Kicks user with given id": "Выгоняет пользователя с заданным ID",
"Labs": "Лаборатория",
"Leave room": "Покинуть комнату",
"Logout": "Выйти",
@ -977,7 +977,7 @@
"Render simple counters in room header": "Отображать простые счетчики в заголовке комнаты",
"Enable Emoji suggestions while typing": "Включить предложения смайликов при наборе",
"Show a placeholder for removed messages": "Показывать плашки вместо удалённых сообщений",
"Show join/leave messages (invites/kicks/bans unaffected)": "Показывать сообщения о входе/выходе (не влияет на приглашения, кики и баны)",
"Show join/leave messages (invites/kicks/bans unaffected)": "Показывать сообщения о входе/выходе (не влияет на приглашения, выгоны и баны)",
"Show avatar changes": "Показывать изменения аватара",
"Show display name changes": "Показывать изменения отображаемого имени",
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Напоминать включить Безопасное Восстановление Сообщений в зашифрованных комнатах",
@ -1276,7 +1276,7 @@
"Join the conversation with an account": "Присоединиться к разговору с учётной записью",
"Sign Up": "Зарегистрироваться",
"Sign In": "Войти",
"You were kicked from %(roomName)s by %(memberName)s": "Вы были выгнаны %(memberName)s из %(roomName)s",
"You were kicked from %(roomName)s by %(memberName)s": "Вы были выгнаны из %(roomName)s пользователем %(memberName)s",
"Reason: %(reason)s": "Причина: %(reason)s",
"Forget this room": "Забыть эту комнату",
"Re-join": "Пере-присоединение",
@ -2122,7 +2122,7 @@
"Enable cross-signing to verify per-user instead of per-session": "Разрешить кросс-подпись для подтверждения пользователей вместо отдельных сессий",
"Keep recovery passphrase in memory for this session": "Сохранить пароль восстановления в памяти для этой сессии",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Отдельно подтверждать каждую сессию пользователя как доверенную, не доверяя кросс-подписанным устройствам.",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Безопасно кэшировать шифрованные сообщения локально, чтобы они появлялись в результатах поиска с помощью ",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Кэшировать шифрованные сообщения локально, чтобы они выводились в результатах поиска, используя: ",
" to store messages from ": " чтобы сохранить сообщения от ",
"Securely cache encrypted messages locally for them to appear in search results.": "Безопасно кэшировать шифрованные сообщения локально, чтобы они появлялись в результатах поиска.",
"Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.": "Отсутствуют некоторые необходимые компоненты для Riot, чтобы безопасно кэшировать шифрованные сообщения локально. Если вы хотите попробовать эту возможность, соберите самостоятельно Riot Desktop с <nativeLink>добавлением поисковых компонентов</nativeLink>.",
@ -2144,5 +2144,7 @@
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong>в %(roomName)s",
"Start verification again from their profile.": "Начните подтверждение заново в профиле пользователя.",
"Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Сообщения в этой комнате зашифрованы сквозным шифрованием. Посмотрите подробности и подтвердите пользователя в его профиле.",
"Send a Direct Message": "Отправить сообщение"
"Send a Direct Message": "Отправить сообщение",
"Light": "Светлая",
"Dark": "Темная"
}

View file

@ -1494,5 +1494,9 @@
"Cannot reach homeserver": "不可连接到主服务器",
"Ensure you have a stable internet connection, or get in touch with the server admin": "确保您的网络连接稳定,或与服务器管理员联系",
"Ask your Riot admin to check <a>your config</a> for incorrect or duplicate entries.": "跟您的Riot管理员确认<a>您的配置</a>不正确或重复的条目。",
"Cannot reach identity server": "不可连接到身份服务器"
"Cannot reach identity server": "不可连接到身份服务器",
"Room name or address": "房间名称或地址",
"Joins room with given address": "使用给定地址加入房间",
"Verify this login": "验证此登录名",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "通过从其他会话之一验证此登录名并授予其访问加密信息的权限来确认您的身份"
}

View file

@ -2525,5 +2525,7 @@
"Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login youll need it to unlock your data.": "建立您的復原金鑰以儲存加密金鑰與您的帳號資料。如果您失去對此登入階段的存取權,您必須用它來解鎖您的資料。",
"Create a Recovery Key": "建立復原金鑰",
"Upgrade your Recovery Key": "升級您的復原金鑰",
"Store your Recovery Key": "儲存您的復原金鑰"
"Store your Recovery Key": "儲存您的復原金鑰",
"Use the improved room list (in development - will refresh to apply changes)": "使用改進的聊天室清單(開發中 ── 將會重新整理以套用變更)",
"Use the improved room list (will refresh to apply changes)": "使用改進的聊天室清單(將會重新整理以套用變更)"
}

View file

@ -134,6 +134,19 @@ export default abstract class BaseEventIndexManager {
throw new Error("Unimplemented");
}
/**
* Check if the room with the given id is already indexed.
*
* @param {string} roomId The ID of the room which we want to check if it
* has been already indexed.
*
* @return {Promise<boolean>} Returns true if the index contains events for
* the given room, false otherwise.
*/
isRoomIndexed(roomId: string): Promise<boolean> {
throw new Error("Unimplemented");
}
/**
* Get statistical information of the index.
*
@ -144,6 +157,29 @@ export default abstract class BaseEventIndexManager {
throw new Error("Unimplemented");
}
/**
* Get the user version of the database.
* @return {Promise<number>} A promise that will resolve to the user stored
* version number.
*/
async getUserVersion(): Promise<number> {
throw new Error("Unimplemented");
}
/**
* Set the user stored version to the given version number.
*
* @param {number} version The new version that should be stored in the
* database.
*
* @return {Promise<void>} A promise that will resolve once the new version
* is stored.
*/
async setUserVersion(version: number): Promise<void> {
throw new Error("Unimplemented");
}
/**
* Commit the previously queued up events to the index.
*

View file

@ -42,9 +42,6 @@ export default class EventIndex extends EventEmitter {
async init() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
await indexManager.initEventIndex();
console.log("EventIndex: Successfully initialized the event index");
this.crawlerCheckpoints = await indexManager.loadCheckpoints();
console.log("EventIndex: Loaded checkpoints", this.crawlerCheckpoints);
@ -62,6 +59,7 @@ export default class EventIndex extends EventEmitter {
client.on('Event.decrypted', this.onEventDecrypted);
client.on('Room.timelineReset', this.onTimelineReset);
client.on('Room.redaction', this.onRedaction);
client.on('RoomState.events', this.onRoomStateEvent);
}
/**
@ -76,6 +74,7 @@ export default class EventIndex extends EventEmitter {
client.removeListener('Event.decrypted', this.onEventDecrypted);
client.removeListener('Room.timelineReset', this.onTimelineReset);
client.removeListener('Room.redaction', this.onRedaction);
client.removeListener('RoomState.events', this.onRoomStateEvent);
}
/**
@ -194,6 +193,15 @@ export default class EventIndex extends EventEmitter {
}
}
onRoomStateEvent = async (ev, state) => {
if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return;
if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) {
console.log("EventIndex: Adding a checkpoint for a newly encrypted room", state.roomId);
this.addRoomCheckpoint(state.roomId, true);
}
}
/*
* The Event.decrypted listener.
*
@ -234,26 +242,12 @@ export default class EventIndex extends EventEmitter {
*/
onTimelineReset = async (room, timelineSet, resetAllTimelines) => {
if (room === null) return;
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
console.log("EventIndex: Adding a checkpoint because of a limited timeline",
room.roomId);
const backwardsCheckpoint = {
roomId: room.roomId,
token: token,
fullCrawl: false,
direction: "b",
};
console.log("EventIndex: Added checkpoint because of a limited timeline",
backwardsCheckpoint);
await indexManager.addCrawlerCheckpoint(backwardsCheckpoint);
this.crawlerCheckpoints.push(backwardsCheckpoint);
this.addRoomCheckpoint(room.roomId, false);
}
/**
@ -334,7 +328,7 @@ export default class EventIndex extends EventEmitter {
avatar_url: ev.sender.getMxcAvatarUrl(),
};
indexManager.addEventToIndex(e, profile);
await indexManager.addEventToIndex(e, profile);
}
/**
@ -345,6 +339,51 @@ export default class EventIndex extends EventEmitter {
this.emit("changedCheckpoint", this.currentRoom());
}
async addEventsFromLiveTimeline(timeline) {
const events = timeline.getEvents();
for (let i = 0; i < events.length; i++) {
const ev = events[i];
await this.addLiveEventToIndex(ev);
}
}
async addRoomCheckpoint(roomId, fullCrawl = false) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
if (!room) return;
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
if (!token) {
// The room doesn't contain any tokens, meaning the live timeline
// contains all the events, add those to the index.
await this.addEventsFromLiveTimeline(timeline);
return;
}
const checkpoint = {
roomId: room.roomId,
token: token,
fullCrawl: fullCrawl,
direction: "b",
};
console.log("EventIndex: Adding checkpoint", checkpoint);
try {
await indexManager.addCrawlerCheckpoint(checkpoint);
} catch (e) {
console.log("EventIndex: Error adding new checkpoint for room",
room.roomId, checkpoint, e);
}
this.crawlerCheckpoints.push(checkpoint);
}
/**
* The main crawler loop.
*
@ -833,6 +872,20 @@ export default class EventIndex extends EventEmitter {
return indexManager.getStats();
}
/**
* Check if the room with the given id is already indexed.
*
* @param {string} roomId The ID of the room which we want to check if it
* has been already indexed.
*
* @return {Promise<boolean>} Returns true if the index contains events for
* the given room, false otherwise.
*/
async isRoomIndexed(roomId) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.isRoomIndexed(roomId);
}
/**
* Get the room that we are currently crawling.
*

View file

@ -23,6 +23,8 @@ import PlatformPeg from "../PlatformPeg";
import EventIndex from "../indexing/EventIndex";
import SettingsStore, {SettingLevel} from '../settings/SettingsStore';
const INDEX_VERSION = 1;
class EventIndexPeg {
constructor() {
this.index = null;
@ -66,8 +68,25 @@ class EventIndexPeg {
*/
async initEventIndex() {
const index = new EventIndex();
const indexManager = PlatformPeg.get().getEventIndexingManager();
try {
await indexManager.initEventIndex();
const userVersion = await indexManager.getUserVersion();
const eventIndexIsEmpty = await indexManager.isEventIndexEmpty();
if (eventIndexIsEmpty) {
await indexManager.setUserVersion(INDEX_VERSION);
} else if (userVersion === 0 && !eventIndexIsEmpty) {
await indexManager.closeEventIndex();
await this.deleteEventIndex();
await indexManager.initEventIndex();
await indexManager.setUserVersion(INDEX_VERSION);
}
console.log("EventIndex: Successfully initialized the event index");
await index.init();
} catch (e) {
console.log("EventIndex: Error initializing the event index", e);

View file

@ -142,7 +142,7 @@ export const SETTINGS = {
},
"feature_new_room_list": {
isFeature: true,
displayName: _td("Use the improved room list (in development - will refresh to apply changes)"),
displayName: _td("Use the improved room list (will refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE,
default: false,
controller: new ReloadOnChangeController(),

View file

@ -15,6 +15,6 @@ limitations under the License.
*/
export default interface IWatcher {
start(): void
stop(): void
start(): void;
stop(): void;
}

View file

@ -20,11 +20,10 @@ import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManage
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
export const PHASE_INTRO = 0;
export const PHASE_RECOVERY_KEY = 1;
export const PHASE_BUSY = 2;
export const PHASE_DONE = 3; //final done stage, but still showing UX
export const PHASE_CONFIRM_SKIP = 4;
export const PHASE_FINISHED = 5; //UX can be closed
export const PHASE_BUSY = 1;
export const PHASE_DONE = 2; //final done stage, but still showing UX
export const PHASE_CONFIRM_SKIP = 3;
export const PHASE_FINISHED = 4; //UX can be closed
export class SetupEncryptionStore extends EventEmitter {
static sharedInstance() {
@ -46,8 +45,17 @@ export class SetupEncryptionStore extends EventEmitter {
// Descriptor of the key that the secrets we want are encrypted with
this.keyInfo = null;
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
const cli = MatrixClientPeg.get();
cli.on("crypto.verification.request", this.onVerificationRequest);
cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged);
const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId());
if (requestsInProgress.length) {
// If there are multiple, we take the most recent. Equally if the user sends another request from
// another device after this screen has been shown, we'll switch to the new one, so this
// generally doesn't support multiple requests.
this._setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]);
}
this.fetchKeyInfo();
}
@ -68,7 +76,7 @@ export class SetupEncryptionStore extends EventEmitter {
async fetchKeyInfo() {
const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false);
if (Object.keys(keys).length === 0) {
if (keys === null || Object.keys(keys).length === 0) {
this.keyId = null;
this.keyInfo = null;
} else {
@ -81,34 +89,7 @@ export class SetupEncryptionStore extends EventEmitter {
this.emit("update");
}
async startKeyReset() {
try {
await accessSecretStorage(() => {}, {forceReset: true});
// If the keys are reset, the trust status event will fire and we'll change state
} catch (e) {
// dialog was cancelled - stay on the current screen
}
}
async useRecoveryKey() {
this.phase = PHASE_RECOVERY_KEY;
this.emit("update");
}
cancelUseRecoveryKey() {
this.phase = PHASE_INTRO;
this.emit("update");
}
async setupWithRecoveryKey(recoveryKey) {
this.startTrustCheck({[this.keyId]: recoveryKey});
}
async usePassPhrase() {
this.startTrustCheck();
}
async startTrustCheck(withKeys) {
this.phase = PHASE_BUSY;
this.emit("update");
const cli = MatrixClientPeg.get();
@ -135,9 +116,6 @@ export class SetupEncryptionStore extends EventEmitter {
// to advance before this.
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
}
}, {
withKeys,
passphraseOnly: true,
}).catch(reject);
} catch (e) {
console.error(e);
@ -168,16 +146,8 @@ export class SetupEncryptionStore extends EventEmitter {
}
}
onVerificationRequest = async (request) => {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.verificationRequest) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
}
this.verificationRequest = request;
await request.accept();
request.on("change", this.onVerificationRequestChange);
this.emit("update");
onVerificationRequest = (request) => {
this._setActiveVerificationRequest(request);
}
onVerificationRequestChange = async () => {
@ -218,4 +188,16 @@ export class SetupEncryptionStore extends EventEmitter {
// async - ask other clients for keys, if necessary
MatrixClientPeg.get()._crypto.cancelAndResendAllOutgoingKeyRequests();
}
async _setActiveVerificationRequest(request) {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.verificationRequest) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
}
this.verificationRequest = request;
await request.accept();
request.on("change", this.onVerificationRequestChange);
this.emit("update");
}
}

View file

@ -92,8 +92,12 @@ export class ListLayout {
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
}
public tilesToPixelsWithPadding(n: number, padding: number): number {
return this.tilesToPixels(n) + padding;
public tilesWithPadding(n: number, paddingPx: number): number {
return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
}
public tilesToPixelsWithPadding(n: number, paddingPx: number): number {
return this.tilesToPixels(n) + paddingPx;
}
public tilesToPixels(n: number): number {

View file

@ -127,7 +127,7 @@ export class Algorithm extends EventEmitter {
const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]);
this.algorithms[tagId] = algorithm;
await algorithm.setRooms(this._cachedRooms[tagId])
await algorithm.setRooms(this._cachedRooms[tagId]);
this._cachedRooms[tagId] = algorithm.orderedRooms;
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
@ -508,16 +508,14 @@ export class Algorithm extends EventEmitter {
return true;
}
if (cause === RoomUpdateCause.NewRoom) {
// TODO: Be smarter and insert rather than regen the planet.
await this.setKnownRooms([room, ...this.rooms]);
return true;
}
if (cause === RoomUpdateCause.RoomRemoved) {
// TODO: Be smarter and splice rather than regen the planet.
await this.setKnownRooms(this.rooms.filter(r => r !== room));
return true;
// If the update is for a room change which might be the sticky room, prevent it. We
// need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though
// as the sticky room relies on this.
if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
if (this.stickyRoom === room) {
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
return false;
}
}
let tags = this.roomIdsToTags[room.roomId];
@ -541,5 +539,5 @@ export class Algorithm extends EventEmitter {
}
return true;
};
}
}

View file

@ -87,7 +87,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm);
console.log("Constructed an ImportanceAlgorithm");
console.log(`[RoomListDebug] Constructed an ImportanceAlgorithm for ${tagId}`);
}
// noinspection JSMethodCanBeStatic
@ -151,8 +151,36 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
}
}
private async handleSplice(room: Room, cause: RoomUpdateCause): Promise<boolean> {
if (cause === RoomUpdateCause.NewRoom) {
const category = this.getRoomCategory(room);
this.alterCategoryPositionBy(category, 1, this.indices);
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
} else if (cause === RoomUpdateCause.RoomRemoved) {
const roomIdx = this.getRoomIndex(room);
if (roomIdx === -1) return false; // no change
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
this.alterCategoryPositionBy(oldCategory, -1, this.indices);
this.cachedOrderedRooms.splice(roomIdx, 1); // remove the room
} else {
throw new Error(`Unhandled splice: ${cause}`);
}
}
private getRoomIndex(room: Room): number {
let roomIdx = this.cachedOrderedRooms.indexOf(room);
if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways.
console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`);
roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId);
}
return roomIdx;
}
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
// TODO: Handle NewRoom and RoomRemoved
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
return this.handleSplice(room, cause);
}
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
throw new Error(`Unsupported update cause: ${cause}`);
}
@ -162,11 +190,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
return; // Nothing to do here.
}
let roomIdx = this.cachedOrderedRooms.indexOf(room);
if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways.
console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`);
roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId);
}
const roomIdx = this.getRoomIndex(room);
if (roomIdx === -1) {
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
}
@ -188,12 +212,18 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
// room from the array.
}
// The room received an update, so take out the slice and sort it. This should be relatively
// quick because the room is inserted at the top of the category, and most popular sorting
// algorithms will deal with trying to keep the active room at the top/start of the category.
// For the few algorithms that will have to move the thing quite far (alphabetic with a Z room
// for example), the list should already be sorted well enough that it can rip through the
// array and slot the changed room in quickly.
// Sort the category now that we've dumped the room in
await this.sortCategory(category);
return true; // change made
}
private async sortCategory(category: Category) {
// This should be relatively quick because the room is usually inserted at the top of the
// category, and most popular sorting algorithms will deal with trying to keep the active
// room at the top/start of the category. For the few algorithms that will have to move the
// thing quite far (alphabetic with a Z room for example), the list should already be sorted
// well enough that it can rip through the array and slot the changed room in quickly.
const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1]
? Number.MAX_SAFE_INTEGER
: this.indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]];
@ -202,8 +232,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort);
const sorted = await sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm);
this.cachedOrderedRooms.splice(startIdx, 0, ...sorted);
return true; // change made
}
// noinspection JSMethodCanBeStatic
@ -230,14 +258,29 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
// We also need to update subsequent categories as they'll all shift by nRooms, so we
// loop over the order to achieve that.
for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) {
const nextCategory = CATEGORY_ORDER[i];
indices[nextCategory] -= nRooms;
}
this.alterCategoryPositionBy(fromCategory, -nRooms, indices);
this.alterCategoryPositionBy(toCategory, +nRooms, indices);
}
for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) {
const nextCategory = CATEGORY_ORDER[i];
indices[nextCategory] += nRooms;
private alterCategoryPositionBy(category: Category, n: number, indices: ICategoryIndex) {
// Note: when we alter a category's index, we actually have to modify the ones following
// the target and not the target itself.
// XXX: If this ever actually gets more than one room passed to it, it'll need more index
// handling. For instance, if 45 rooms are removed from the middle of a 50 room list, the
// index for the categories will be way off.
const nextOrderIndex = CATEGORY_ORDER.indexOf(category) + 1;
if (n > 0) {
for (let i = nextOrderIndex; i < CATEGORY_ORDER.length; i++) {
const nextCategory = CATEGORY_ORDER[i];
indices[nextCategory] += Math.abs(n);
}
} else if (n < 0) {
for (let i = nextOrderIndex; i < CATEGORY_ORDER.length; i++) {
const nextCategory = CATEGORY_ORDER[i];
indices[nextCategory] -= Math.abs(n);
}
}
// Do a quick check to see if we've completely broken the index

View file

@ -28,7 +28,7 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm);
console.log("Constructed a NaturalAlgorithm");
console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`);
}
public async setRooms(rooms: Room[]): Promise<any> {
@ -36,11 +36,19 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
}
public async handleRoomUpdate(room, cause): Promise<boolean> {
// TODO: Handle NewRoom and RoomRemoved
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt;
if (!isSplice && !isInPlace) {
throw new Error(`Unsupported update cause: ${cause}`);
}
if (cause === RoomUpdateCause.NewRoom) {
this.cachedOrderedRooms.push(room);
} else if (cause === RoomUpdateCause.RoomRemoved) {
const idx = this.cachedOrderedRooms.indexOf(room);
if (idx >= 0) this.cachedOrderedRooms.splice(idx, 1);
}
// TODO: Optimize this to avoid useless operations
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);

View file

@ -67,6 +67,5 @@ export abstract class OrderingAlgorithm {
* @param cause The cause of the update.
* @returns True if the update requires the Algorithm to update the presentation layers.
*/
// XXX: TODO: We assume this will only ever be a position update and NOT a NewRoom or RemoveRoom change!!
public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean>;
}

View file

@ -36,7 +36,7 @@ export function formatCount(count: number): string {
*/
export function formatCountLong(count: number): string {
const formatter = new Intl.NumberFormat();
return formatter.format(count)
return formatter.format(count);
}
/**

View file

@ -10,7 +10,7 @@ interface Client {
getStoredDevicesForUser: (userId: string) => [{ deviceId: string }];
checkDeviceTrust: (userId: string, deviceId: string) => {
isVerified: () => boolean
}
};
}
interface Room {

View file

@ -81,7 +81,34 @@ describe('QueryMatcher', function() {
expect(reverseResults[1].name).toBe('Victoria');
});
it('Returns results with search string in same place in insertion order', function() {
it('Returns results with search string in same place according to key index', function() {
const objects = [
{ name: "a", first: "hit", second: "miss", third: "miss" },
{ name: "b", first: "miss", second: "hit", third: "miss" },
{ name: "c", first: "miss", second: "miss", third: "hit" },
];
const qm = new QueryMatcher(objects, {keys: ["second", "first", "third"]});
const results = qm.match('hit');
expect(results.length).toBe(3);
expect(results[0].name).toBe('b');
expect(results[1].name).toBe('a');
expect(results[2].name).toBe('c');
qm.setObjects(objects.slice().reverse());
const reverseResults = qm.match('hit');
// should still be in the same order: key index
// takes precedence over input order
expect(reverseResults.length).toBe(3);
expect(reverseResults[0].name).toBe('b');
expect(reverseResults[1].name).toBe('a');
expect(reverseResults[2].name).toBe('c');
});
it('Returns results with search string in same place and key in same place in insertion order', function() {
const qm = new QueryMatcher(OBJECTS, {keys: ["name"]});
const results = qm.match('Mel');
@ -132,9 +159,9 @@ describe('QueryMatcher', function() {
const results = qm.match('Emma');
expect(results.length).toBe(3);
expect(results[0].name).toBe('Mel B');
expect(results[1].name).toBe('Mel C');
expect(results[2].name).toBe('Emma');
expect(results[0].name).toBe('Emma');
expect(results[1].name).toBe('Mel B');
expect(results[2].name).toBe('Mel C');
});
it('Matches words only by default', function() {

View file

@ -79,7 +79,20 @@ module.exports = async function signup(session, username, password, homeserver)
const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit');
await acceptButton.click();
const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary');
//plow through cross-signing setup by entering arbitrary details
//TODO: It's probably important for the tests to know the passphrase
const xsigningPassphrase = 'a7eaXcjpa9!Yl7#V^h$B^%dovHUVX'; // https://xkcd.com/221/
let passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input');
await session.replaceInputText(passphraseField, xsigningPassphrase);
await session.delay(1000); // give it a second to analyze our passphrase for security
let xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary');
await xsignContButton.click();
//repeat passphrase entry
passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input');
await session.replaceInputText(passphraseField, xsigningPassphrase);
await session.delay(1000); // give it a second to analyze our passphrase for security
xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary');
await xsignContButton.click();
//ignore the recovery key
@ -88,11 +101,13 @@ module.exports = async function signup(session, username, password, homeserver)
await copyButton.click();
//acknowledge that we copied the recovery key to a safe place
const copyContinueButton = await session.query(
'.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary',
);
const copyContinueButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary');
await copyContinueButton.click();
//acknowledge that we're done cross-signing setup and our keys are safe
const doneOkButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary');
await doneOkButton.click();
//wait for registration to finish so the hash gets set
//onhashchange better?

View file

@ -46,7 +46,9 @@
"quotemark": false,
"radix": true,
"semicolon": [
"always"
true,
"always",
"strict-bound-class-methods"
],
"triple-equals": [],
"typedef-whitespace": [