Merge branch 'develop' into joriks/font-scaling-message-preview

This commit is contained in:
Jorik Schellekens 2020-06-18 15:19:28 +01:00 committed by GitHub
commit 15881fc7d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
101 changed files with 1697 additions and 739 deletions

View file

@ -1,3 +1,9 @@
Changes in [2.7.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.2) (2020-06-16)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.1...v2.7.2)
* Upgrade to JS SDK 6.2.2
Changes in [2.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.1) (2020-06-05)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0...v2.7.1)

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "2.7.1",
"version": "2.7.2",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {

View file

@ -189,6 +189,7 @@
@import "./views/rooms/_RoomSublist2.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTile2.scss";
@import "./views/rooms/_RoomTileIcon.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SendMessageComposer.scss";

View file

@ -131,6 +131,7 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
overflow-y: auto;
width: 100%;
max-width: 100%;
position: relative; // for sticky headers
// Create a flexbox to trick the layout engine
display: flex;

View file

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

View file

@ -146,3 +146,12 @@ limitations under the License.
.mx_AuthBody_spinner {
margin: 1em 0;
}
@media only screen and (max-width: 480px) {
.mx_AuthBody {
border-radius: 4px;
width: auto;
max-width: 500px;
padding: 10px;
}
}

View file

@ -21,3 +21,9 @@ limitations under the License.
padding: 25px 40px;
box-sizing: border-box;
}
@media only screen and (max-width: 480px) {
.mx_AuthHeader {
display: none;
}
}

View file

@ -23,3 +23,9 @@ limitations under the License.
.mx_AuthHeaderLogo img {
width: 100%;
}
@media only screen and (max-width: 480px) {
.mx_AuthHeaderLogo {
display: none;
}
}

View file

@ -29,3 +29,9 @@ limitations under the License.
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.33);
background-color: $authpage-modal-bg-color;
}
@media only screen and (max-width: 480px) {
.mx_AuthPage_modal {
margin-top: 0;
}
}

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

@ -70,5 +70,15 @@ limitations under the License.
& + label > *:not(.mx_Checkbox_background) {
margin-left: 10px;
}
&:disabled + label {
opacity: 0.5;
cursor: not-allowed;
}
&:checked:disabled + label > .mx_Checkbox_background {
background-color: $muted-fg-color;
border-color: rgba($muted-fg-color, 0.5);
}
}
}

View file

@ -20,7 +20,6 @@ limitations under the License.
*/
.mx_RadioButton {
$radio-circle-color: $muted-fg-color;
$active-radio-circle-color: $accent-color;
position: relative;
@ -76,22 +75,32 @@ limitations under the License.
border-radius: $font-8px;
}
}
}
> input[type=radio]:checked {
+ div {
border-color: $active-radio-circle-color;
&:checked {
& + div {
border-color: $active-radio-circle-color;
> div {
background: $active-radio-circle-color;
& > div {
background: $active-radio-circle-color;
}
}
}
}
> input[type=radio]:disabled {
+ div {
> div {
display: none;
&:disabled {
& + div,
& + div + span {
opacity: 0.5;
cursor: not-allowed;
}
& + div {
border-color: $radio-circle-color;
}
}
&:checked:disabled {
& + div > div {
background-color: $radio-circle-color;
}
}
}

View file

@ -572,3 +572,14 @@ limitations under the License.
margin-left: 1em;
}
}
@media only screen and (max-width: 480px) {
.mx_EventTile_line, .mx_EventTile_reply {
padding-left: 0;
margin-right: 0;
}
.mx_EventTile_content {
margin-top: 10px;
margin-right: 0;
}
}

View file

@ -267,3 +267,12 @@ limitations under the License.
.mx_RoomHeader_pinsIndicatorUnread {
background-color: $pinned-unread-color;
}
@media only screen and (max-width: 480px) {
.mx_RoomHeader_wrapper {
padding: 0;
}
.mx_RoomHeader {
overflow: hidden;
}
}

View file

@ -27,12 +27,61 @@ limitations under the License.
width: 100%;
.mx_RoomSublist2_headerContainer {
// Create a flexbox to make ordering easy
// Create a flexbox to make alignment easy
display: flex;
align-items: center;
// ***************************
// Sticky Headers Start
// Ideally we'd be able to use `position: sticky; top: 0; bottom: 0;` on the
// headerContainer, however due to our layout concerns we actually have to
// calculate it manually so we can sticky things in the right places. We also
// target the headerText instead of the container to reduce jumps when scrolling,
// and to help hide the badges/other buttons that could appear on hover. This
// all works by ensuring the header text has a fixed height when sticky so the
// fixed height of the container can maintain the scroll position.
// The combined height must be set in the LeftPanel2 component for sticky headers
// to work correctly.
padding-bottom: 8px;
height: 24px;
.mx_RoomSublist2_stickable {
flex: 1;
max-width: 100%;
z-index: 2; // Prioritize headers in the visible list over sticky ones
// Set the same background color as the room list for sticky headers
background-color: $roomlist2-bg-color;
// Create a flexbox to make ordering easy
display: flex;
align-items: center;
// We use a generic sticky class for 2 reasons: to reduce style duplication and
// to identify when a header is sticky. If we didn't have a consistent sticky class,
// we'd have to do the "is sticky" checks again on click, as clicking the header
// when sticky scrolls instead of collapses the list.
&.mx_RoomSublist2_headerContainer_sticky {
position: fixed;
z-index: 1; // over top of other elements, but still under the ones in the visible list
height: 32px; // to match the header container
// width set by JS
}
&.mx_RoomSublist2_headerContainer_stickyBottom {
bottom: 0;
}
// We don't have a top style because the top is dependent on the room list header's
// height, and is therefore calculated in JS.
// The class, mx_RoomSublist2_headerContainer_stickyTop, is applied though.
}
// Sticky Headers End
// ***************************
.mx_RoomSublist2_badgeContainer {
opacity: 0.8;
width: 16px;
@ -76,18 +125,45 @@ limitations under the License.
}
.mx_RoomSublist2_headerText {
flex: 1;
max-width: calc(100% - 16px); // 16px is the badge width
text-transform: uppercase;
opacity: 0.5;
line-height: $font-16px;
font-size: $font-12px;
flex: 1;
max-width: calc(100% - 16px); // 16px is the badge width
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.mx_RoomSublist2_collapseBtn {
display: inline-block;
position: relative;
// Default hidden
visibility: hidden;
width: 0;
height: 0;
&::before {
content: '';
width: 12px;
height: 12px;
position: absolute;
top: 1px;
left: 1px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
&.mx_RoomSublist2_collapseBtn_collapsed::before {
mask-image: url('$(res)/img/feather-customised/chevron-right.svg');
}
}
}
}
@ -100,7 +176,7 @@ limitations under the License.
flex-direction: column;
overflow: hidden;
.mx_RoomSublist2_showMoreButton {
.mx_RoomSublist2_showNButton {
cursor: pointer;
font-size: $font-13px;
line-height: $font-18px;
@ -129,18 +205,25 @@ limitations under the License.
display: flex;
align-items: center;
.mx_RoomSublist2_showMoreButtonChevron {
.mx_RoomSublist2_showNButtonChevron {
position: relative;
width: 16px;
height: 16px;
margin-left: 12px;
margin-right: 18px;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $roomtile2-preview-color;
}
.mx_RoomSublist2_showMoreButtonChevron {
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
.mx_RoomSublist2_showLessButtonChevron {
mask-image: url('$(res)/img/feather-customised/chevron-up.svg');
}
}
// Class name comes from the ResizableBox component
@ -201,6 +284,17 @@ limitations under the License.
background-color: $roomlist2-button-bg-color;
}
}
.mx_RoomSublist2_headerContainer {
.mx_RoomSublist2_headerText {
.mx_RoomSublist2_collapseBtn {
visibility: visible;
width: 12px;
height: 12px;
margin-right: 4px;
}
}
}
}
&.mx_RoomSublist2_minimized {
@ -239,10 +333,10 @@ limitations under the License.
.mx_RoomSublist2_resizeBox {
align-items: center;
.mx_RoomSublist2_showMoreButton {
.mx_RoomSublist2_showNButton {
flex-direction: column;
.mx_RoomSublist2_showMoreButtonChevron {
.mx_RoomSublist2_showNButtonChevron {
margin-right: 12px; // to center
}
}
@ -320,8 +414,4 @@ limitations under the License.
.mx_RadioButton, .mx_Checkbox {
margin-top: 8px;
}
.mx_Checkbox {
margin-left: -8px; // to counteract the indent from the component
}
}

View file

@ -32,6 +32,13 @@ limitations under the License.
.mx_RoomTile2_avatarContainer {
margin-right: 8px;
position: relative;
.mx_RoomTileIcon {
position: absolute;
bottom: 0;
right: 0;
}
}
.mx_RoomTile2_nameContainer {

View file

@ -0,0 +1,69 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomTileIcon {
width: 12px;
height: 12px;
border-radius: 12px;
background-color: $roomlist2-bg-color; // to match the room list itself
}
.mx_RoomTileIcon_globe::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
mask-image: url('$(res)/img/globe.svg');
}
.mx_RoomTileIcon_offline::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
border-radius: 8px;
background-color: $presence-offline;
}
.mx_RoomTileIcon_online::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
border-radius: 8px;
background-color: $presence-online;
}
.mx_RoomTileIcon_away::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
border-radius: 8px;
background-color: $presence-away;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>

After

Width:  |  Height:  |  Size: 270 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>

After

Width:  |  Height:  |  Size: 268 B

6
res/img/globe.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="sea" fill-rule="evenodd" clip-rule="evenodd" d="M4 8C6.20914 8 8 6.20914 8 4C8 1.79086 6.20914 0 4 0C1.79086 0 0 1.79086 0 4C0 6.20914 1.79086 8 4 8ZM4.6693 2.43613C4.8306 2.64728 4.94732 2.80007 4.45289 2.80007C4.14732 2.80007 3.84175 2.74171 3.58076 2.69186C3.15847 2.61121 2.85289 2.55285 2.85289 2.80007C2.85289 3.00007 3.65289 3.40007 4.45289 3.80007C5.25289 4.20007 6.05289 4.60007 6.05289 4.80007C6.05289 5.20007 6.05289 7.60007 5.25289 7.20007C4.45289 6.80007 2.45289 5.20007 2.45289 4.80007C2.45289 4.65277 2.18168 4.39698 1.85897 4.09263C1.30535 3.57051 0.600192 2.90547 0.852893 2.40007C1.25289 1.60007 2.85289 6.51479e-05 5.25289 0.800065C4.98623 1.06673 4.45289 1.68007 4.45289 2.00007C4.45289 2.15285 4.56961 2.30564 4.6693 2.43613Z" fill="#2E2F32"/>
<path id="earth" d="M4.45294 2.80007C5.25294 2.80007 4.45294 2.40007 4.45294 2.00007C4.45294 1.68007 4.98627 1.06673 5.25294 0.800065C2.85294 6.51479e-05 1.25294 1.60007 0.852941 2.40007C0.452941 3.20007 2.45294 4.40007 2.45294 4.80007C2.45294 5.20007 4.45294 6.80007 5.25294 7.20007C6.05294 7.60007 6.05294 5.20007 6.05294 4.80007C6.05294 4.40007 2.85294 3.20007 2.85294 2.80007C2.85294 2.40007 3.65294 2.80007 4.45294 2.80007Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -186,6 +186,10 @@ $roomtile2-preview-color: #9e9e9e;
$roomtile2-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #FFF;
$presence-online: $accent-color;
$presence-away: orange; // TODO: Get color
$presence-offline: #E3E8F0;
// ********************
$roomtile-name-color: #61708b;

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

@ -20,7 +20,6 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import { _t } from './languageHandler';
import SettingsStore from './settings/SettingsStore';
import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
// This stores the secret storage private keys in memory for the JS SDK. This is
@ -30,14 +29,9 @@ 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 ||
SettingsStore.getValue("keepSecretStoragePassphraseForSession")
);
return secretStorageBeingAccessed;
}
export class AccessCancelledError extends Error {
@ -101,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,
@ -216,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,
);
@ -274,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

@ -95,6 +95,8 @@ export default class Login {
identifier = {
type: 'm.id.phone',
country: phoneCountry,
phone: phoneNumber,
// XXX: Synapse historically wanted `number` and not `phone`
number: phoneNumber,
};
} else if (isEmail) {

View file

@ -175,14 +175,6 @@ export default class Markdown {
const renderer = new commonmark.HtmlRenderer({safe: false});
const real_paragraph = renderer.paragraph;
// The default `out` function only sends the input through an XML
// escaping function, which causes messages to be entity encoded,
// which we don't want in this case.
renderer.out = function(s) {
// The `lit` function adds a string literal to the output buffer.
this.lit(s);
};
renderer.paragraph = function(node, entering) {
// as with toHTML, only append lines to paragraphs if there are
// multiple paragraphs

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

@ -118,7 +118,7 @@ export class Command {
run(roomId: string, args: string, cmd: string) {
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
if (!this.runFn) return;
if (!this.runFn) return reject(_t("Command error"));
return this.runFn.bind(this)(roomId, args, cmd);
}

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

@ -86,6 +86,45 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
}
};
// TODO: Apply this on resize, init, etc for reliability
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
const list = ev.target as HTMLDivElement;
const rlRect = list.getBoundingClientRect();
const bottom = rlRect.bottom;
const top = rlRect.top;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
const headerHeight = 32; // Note: must match the CSS!
const headerRightMargin = 24; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = rlRect.width - headerRightMargin;
let gotBottom = false;
for (const sublist of sublists) {
const slRect = sublist.getBoundingClientRect();
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
if (slRect.top + headerHeight > bottom && !gotBottom) {
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");
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
header.style.width = `${headerStickyWidth}px`;
header.style.top = `${rlRect.top}px`;
} else {
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `unset`;
header.style.top = `unset`;
}
}
};
private renderHeader(): React.ReactNode {
// TODO: Update when profile info changes
// TODO: Presence
@ -191,7 +230,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
<aside className="mx_LeftPanel2_roomListContainer">
{this.renderHeader()}
{this.renderSearchExplore()}
<div className="mx_LeftPanel2_actualRoomListContainer">
<div className="mx_LeftPanel2_actualRoomListContainer" onScroll={this.onScroll}>
{roomList}
</div>
</aside>

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 {

View file

@ -1977,8 +1977,9 @@ export default createReactClass({
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
} else {
searchResultsPanel = (
<ScrollPanel ref={this._searchResultsPanel}
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
<ScrollPanel
ref={this._searchResultsPanel}
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel mx_GroupLayout"
onFillRequest={this.onSearchResultsFillRequest}
resizeNotifier={this.props.resizeNotifier}
>

View file

@ -32,6 +32,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import {getCustomTheme} from "../../theme";
import {getHostingLink} from "../../utils/HostingLink";
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
import {getHomePageUrl} from "../../utils/pages";
interface IProps {
}
@ -67,6 +69,10 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
}
}
private get hasHomePage(): boolean {
return !!getHomePageUrl(SdkConfig.get());
}
public componentDidMount() {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
@ -147,6 +153,13 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
this.setState({menuDisplayed: false}); // also close the menu
};
private onHomeClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({action: 'view_home_page'});
};
public render() {
let contextMenu;
if (this.state.menuDisplayed) {
@ -172,6 +185,18 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
);
}
let homeButton = null;
if (this.hasHomePage) {
homeButton = (
<li>
<AccessibleButton onClick={this.onHomeClick}>
<img src={require("../../../res/img/feather-customised/home.svg")} width={16} />
<span>{_t("Home")}</span>
</AccessibleButton>
</li>
);
}
const elementRect = this.buttonRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu
@ -205,6 +230,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
<ul>
{homeButton}
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<img src={require("../../../res/img/feather-customised/notifications.svg")} width={16} />
@ -265,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

@ -19,26 +19,15 @@ 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,
PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore';
function keyHasPassphrase(keyInfo) {
return (
keyInfo.passphrase &&
keyInfo.passphrase.salt &&
keyInfo.passphrase.iterations
);
}
export default class SetupEncryptionBody extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
@ -56,11 +45,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 +67,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 +92,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");
@ -194,13 +108,6 @@ export default class SetupEncryptionBody extends React.Component {
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
} else {
recoveryKeyPrompt = _t("Use Recovery Key");
}
return (
<div>
<p>{_t(
@ -224,67 +131,15 @@ export default class SetupEncryptionBody extends React.Component {
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="link" onClick={this._onUseRecoveryKeyClick}>
{recoveryKeyPrompt}
<AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
{_t("Use Recovery Passphrase or Key")}
</AccessibleButton>
<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

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { debounce } from 'lodash';
import {IFieldState, IValidationResult} from "../elements/Validation";
import {IFieldState, IValidationResult} from "./Validation";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@ -29,60 +29,76 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInputElement> {
interface IProps {
// The field's ID, which binds the input and label together. Immutable.
id?: string,
// The element to create. Defaults to "input".
// To define options for a select, use <Field><option ... /></Field>
element?: "input" | "select" | "textarea",
id?: string;
// The field's type (when used as an <input>). Defaults to "text".
type?: string,
type?: string;
// id of a <datalist> element for suggestions
list?: string,
list?: string;
// The field's label string.
label?: string,
label?: string;
// The field's placeholder string. Defaults to the label.
placeholder?: string,
// The field's value.
// This is a controlled component, so the value is required.
value: string,
placeholder?: string;
// Optional component to include inside the field before the input.
prefixComponent?: React.ReactNode,
prefixComponent?: React.ReactNode;
// Optional component to include inside the field after the input.
postfixComponent?: React.ReactNode,
postfixComponent?: React.ReactNode;
// The callback called whenever the contents of the field
// changes. Returns an object with `valid` boolean field
// and a `feedback` react component field to provide feedback
// to the user.
onValidate?: (input: IFieldState) => Promise<IValidationResult>,
onValidate?: (input: IFieldState) => Promise<IValidationResult>;
// If specified, overrides the value returned by onValidate.
flagInvalid?: boolean,
flagInvalid?: boolean;
// If specified, contents will appear as a tooltip on the element and
// validation feedback tooltips will be suppressed.
tooltipContent?: React.ReactNode,
tooltipContent?: React.ReactNode;
// If specified alongside tooltipContent, the class name to apply to the
// tooltip itself.
tooltipClassName?: string,
tooltipClassName?: string;
// If specified, an additional class name to apply to the field container
className?: string,
className?: string;
// All other props pass through to the <input>.
}
interface IState {
valid: boolean,
feedback: React.ReactNode,
feedbackVisible: boolean,
focused: boolean,
interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
// The element to create. Defaults to "input".
element?: "input";
// The input's value. This is a controlled component, so the value is required.
value: string;
}
export default class Field extends React.PureComponent<IProps, IState> {
interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
// To define options for a select, use <Field><option ... /></Field>
element: "select";
// The select's value. This is a controlled component, so the value is required.
value: string;
}
interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
element: "textarea";
// The textarea's value. This is a controlled component, so the value is required.
value: string;
}
type PropShapes = IInputProps | ISelectProps | ITextareaProps;
interface IState {
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

@ -748,19 +748,26 @@ const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpd
powerLevels.state_default
);
// if these do not exist in the event then they should default to 50 as per the spec
const {
ban: banPowerLevel = 50,
kick: kickPowerLevel = 50,
redact: redactPowerLevel = 50,
} = powerLevels;
const me = room.getMember(cli.getUserId());
const isMe = me.userId === member.userId;
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
if (canAffectUser && me.powerLevel >= powerLevels.kick) {
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
}
if (me.powerLevel >= powerLevels.redact) {
if (me.powerLevel >= redactPowerLevel) {
redactButton = (
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
);
}
if (canAffectUser && me.powerLevel >= powerLevels.ban) {
if (canAffectUser && me.powerLevel >= banPowerLevel) {
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
}
if (canAffectUser && me.powerLevel >= editPowerLevel) {

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,12 @@ 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
};
private onShowLessClick = () => {
this.props.layout.visibleTiles = this.props.layout.minVisibleTiles;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
@ -134,7 +144,28 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private onHeaderClick = (ev: React.MouseEvent<HTMLDivElement>) => {
let target = ev.target as HTMLDivElement;
if (!target.classList.contains('mx_RoomSublist2_headerText')) {
// If we don't have the headerText class, the user clicked the span in the headerText.
target = target.parentElement as HTMLDivElement;
}
const possibleSticky = target.parentElement;
const sublist = possibleSticky.parentElement.parentElement;
if (possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky')) {
// is sticky - jump to list
sublist.scrollIntoView({behavior: 'smooth'});
} else {
// on screen - toggle collapse
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update
}
};
private renderTiles(): React.ReactElement[] {
if (this.props.layout && this.props.layout.isCollapsed) return []; // don't waste time on rendering
const tiles: React.ReactElement[] = [];
if (this.props.rooms) {
@ -145,6 +176,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
key={`room-${room.roomId}`}
showMessagePreview={this.props.layout.showPreviews}
isMinimized={this.props.isMinimized}
tag={this.props.layout.tagId}
/>
);
}
@ -249,6 +281,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
);
}
const collapseClasses = classNames({
'mx_RoomSublist2_collapseBtn': true,
'mx_RoomSublist2_collapseBtn_collapsed': this.props.layout && this.props.layout.isCollapsed,
});
const classes = classNames({
'mx_RoomSublist2_headerContainer': true,
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
@ -257,19 +294,23 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// TODO: a11y (see old component)
return (
<div className={classes}>
<AccessibleButton
inputRef={ref}
tabIndex={tabIndex}
className={"mx_RoomSublist2_headerText"}
role="treeitem"
aria-level={1}
>
<span>{this.props.label}</span>
</AccessibleButton>
{this.renderMenu()}
{addRoomButton}
<div className="mx_RoomSublist2_badgeContainer">
{badge}
<div className='mx_RoomSublist2_stickable'>
<AccessibleButton
inputRef={ref}
tabIndex={tabIndex}
className={"mx_RoomSublist2_headerText"}
role="treeitem"
aria-level={1}
onClick={this.onHeaderClick}
>
<span className={collapseClasses} />
<span>{this.props.label}</span>
</AccessibleButton>
{this.renderMenu()}
{addRoomButton}
<div className="mx_RoomSublist2_badgeContainer">
{badge}
</div>
</div>
</div>
);
@ -303,25 +344,42 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const visibleTiles = tiles.slice(0, nVisible);
// If we're hiding rooms, show a 'show more' button to the user. This button
// floats above the resize handle, if we have one present
let showMoreButton = null;
// floats above the resize handle, if we have one present. If the user has all
// tiles visible, it becomes 'show less'.
let showNButton = null;
if (tiles.length > nVisible) {
// we have a cutoff condition - add the button to show all
const numMissing = tiles.length - visibleTiles.length;
let showMoreText = (
<span className='mx_RoomSublist2_showMoreButtonText'>
<span className='mx_RoomSublist2_showNButtonText'>
{_t("Show %(count)s more", {count: numMissing})}
</span>
);
if (this.props.isMinimized) showMoreText = null;
showMoreButton = (
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showMoreButton'>
<span className='mx_RoomSublist2_showMoreButtonChevron'>
showNButton = (
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showNButton'>
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
{/* set by CSS masking */}
</span>
{showMoreText}
</div>
);
} 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'>
{_t("Show less")}
</span>
);
if (this.props.isMinimized) showLessText = null;
showNButton = (
<div onClick={this.onShowLessClick} className='mx_RoomSublist2_showNButton'>
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
{/* set by CSS masking */}
</span>
{showLessText}
</div>
);
}
// Figure out if we need a handle
@ -340,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 (showMoreButton) 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
@ -365,9 +421,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
className="mx_RoomSublist2_resizeBox"
>
{visibleTiles}
{showMoreButton}
{showNButton}
</ResizableBox>
)
);
}
// TODO: onKeyDown support

View file

@ -21,7 +21,7 @@ import React, { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, {ButtonEvent} from "../../views/elements/AccessibleButton";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import RoomAvatar from "../../views/avatars/RoomAvatar";
import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
@ -31,6 +31,7 @@ import { _t } from "../../../languageHandler";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
import RoomTileIcon from "./RoomTileIcon";
/*******************************************************************
* CAUTION *
@ -44,6 +45,7 @@ interface IProps {
room: Room;
showMessagePreview: boolean;
isMinimized: boolean;
tag: TagID;
// TODO: Allow falsifying counts (for invites and stuff)
// TODO: Transparency? Was this ever used?
@ -230,7 +232,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
/>
{contextMenu}
</React.Fragment>
)
);
}
public render(): React.ReactElement {
@ -303,7 +305,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
role="treeitem"
>
<div className="mx_RoomTile2_avatarContainer">
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/>
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} />
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
</div>
{nameContainer}
<div className="mx_RoomTile2_badgeContainer">

View file

@ -0,0 +1,150 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { User } from "matrix-js-sdk/src/models/user";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import DMRoomMap from "../../../utils/DMRoomMap";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { isPresenceEnabled } from "../../../utils/presence";
enum Icon {
// Note: the names here are used in CSS class names
None = "NONE", // ... except this one
Globe = "GLOBE",
PresenceOnline = "ONLINE",
PresenceAway = "AWAY",
PresenceOffline = "OFFLINE",
}
interface IProps {
room: Room;
tag: TagID;
}
interface IState {
icon: Icon;
}
export default class RoomTileIcon extends React.Component<IProps, IState> {
private _dmUser: User;
private isUnmounted = false;
private isWatchingTimeline = false;
constructor(props: IProps) {
super(props);
this.state = {
icon: this.calculateIcon(),
};
}
private get isPublicRoom(): boolean {
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule;
return joinRule === 'public';
}
private get dmUser(): User {
return this._dmUser;
}
private set dmUser(val: User) {
const oldUser = this._dmUser;
this._dmUser = val;
if (oldUser && oldUser !== this._dmUser) {
oldUser.off('User.currentlyActive', this.onPresenceUpdate);
oldUser.off('User.presence', this.onPresenceUpdate);
}
if (this._dmUser && oldUser !== this._dmUser) {
this._dmUser.on('User.currentlyActive', this.onPresenceUpdate);
this._dmUser.on('User.presence', this.onPresenceUpdate);
}
}
public componentWillUnmount() {
this.isUnmounted = true;
if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline);
this.dmUser = null; // clear listeners, if any
}
private onRoomTimeline = (ev: MatrixEvent, room: Room) => {
if (this.isUnmounted) return;
// apparently these can happen?
if (!room) return;
if (this.props.room.roomId !== room.roomId) return;
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
this.setState({icon: this.calculateIcon()});
}
};
private onPresenceUpdate = () => {
if (this.isUnmounted) return;
let newIcon = this.getPresenceIcon();
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
};
private getPresenceIcon(): Icon {
if (!this.dmUser) return Icon.None;
let icon = Icon.None;
const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online';
if (isOnline) {
icon = Icon.PresenceOnline;
} else if (this.dmUser.presence === 'offline') {
icon = Icon.PresenceOffline;
} else if (this.dmUser.presence === 'unavailable') {
icon = Icon.PresenceAway;
}
return icon;
}
private calculateIcon(): Icon {
let icon = Icon.None;
if (this.props.tag === DefaultTagID.DM && this.props.room.getJoinedMemberCount() === 2) {
// Track presence, if available
if (isPresenceEnabled()) {
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
if (otherUserId) {
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
icon = this.getPresenceIcon();
}
}
} else {
// Track publicity
icon = this.isPublicRoom ? Icon.Globe : Icon.None;
if (!this.isWatchingTimeline) {
this.props.room.on('Room.timeline', this.onRoomTimeline);
this.isWatchingTimeline = true;
}
}
return icon;
}
public render(): React.ReactElement {
if (this.state.icon === Icon.None) return null;
return <span className={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`} />;
}
}

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

@ -45,7 +45,7 @@ interface IThemeState {
export interface CustomThemeMessage {
isError: boolean;
text: string;
};
}
interface IState extends IThemeState {
// String displaying the current selected fontSize.
@ -162,7 +162,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

@ -66,7 +66,6 @@ export default class LabsUserSettingsTab extends React.Component {
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"keepSecretStoragePassphraseForSession"} level={SettingLevel.DEVICE} />
</div>
</div>
);

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

@ -42,6 +42,10 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
if (!parser.isPlainText() || forceHTML) {
return parser.toHTML();
}
// ensure removal of escape backslashes in non-Markdown messages
if (md.indexOf("\\") > -1) {
return parser.toPlaintext();
}
}
export function textSerialize(model: EditorModel) {
@ -62,16 +66,20 @@ export function textSerialize(model: EditorModel) {
}
export function containsEmote(model: EditorModel) {
return startsWith(model, "/me ");
return startsWith(model, "/me ", false);
}
export function startsWith(model: EditorModel, prefix: string) {
export function startsWith(model: EditorModel, prefix: string, caseSensitive = true) {
const firstPart = model.parts[0];
// part type will be "plain" while editing,
// and "command" while composing a message.
return firstPart &&
(firstPart.type === "plain" || firstPart.type === "command") &&
firstPart.text.startsWith(prefix);
let text = firstPart && firstPart.text;
if (!caseSensitive) {
prefix = prefix.toLowerCase();
text = text.toLowerCase();
}
return firstPart && (firstPart.type === "plain" || firstPart.type === "command") && text.startsWith(prefix);
}
export function stripEmoteCommand(model: EditorModel) {

View file

@ -1367,7 +1367,7 @@
"Sends the given message coloured as a rainbow": "Sendet die Nachricht in Regenbogenfarben",
"Adds a custom widget by URL to the room": "Fügt ein Benutzer-Widget über eine URL zum Raum hinzu",
"Please supply a https:// or http:// widget URL": "Bitte gib eine https:// oder http:// Widget-URL an",
"Sends the given emote coloured as a rainbow": "Sended das Emoji in Regenbogenfarben",
"Sends the given emote coloured as a rainbow": "Sendet das Emoji in Regenbogenfarben",
"%(senderName)s made no change.": "%(senderName)s hat keine Änderung vorgenommen.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s hat die Einladung zum Raumbeitritt für %(targetDisplayName)s zurückgezogen.",
"Cannot reach homeserver": "Der Heimserver ist nicht erreichbar",
@ -2449,5 +2449,18 @@
"Show %(count)s more|other": "Zeige %(count)s weitere",
"Show %(count)s more|one": "Zeige %(count)s weitere",
"Leave Room": "Verlasse Raum",
"Room options": "Raumoptionen"
"Room options": "Raumoptionen",
"Activity": "Aktivität",
"A-Z": "A-Z",
"Recovery Key": "Wiederherstellungsschlüssel",
"This isn't the recovery key for your account": "Das ist nicht der Wiederherstellungsschlüssel für dein Konto",
"This isn't a valid recovery key": "Das ist kein gültiger Wiederherstellungsschlüssel",
"Looks good!": "Sieht gut aus!",
"Use Recovery Key or Passphrase": "Verwende einen Wiederherstellungsschlüssel oder deine Passphrase",
"Use Recovery Key": "Verwende einen Wiederherstellungsschlüssel",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "Gib deinen Wiederherstellungsschlüssel oder eine <a>Wiederherstellungspassphrase</a> ein um fortzufahren.",
"Enter your Recovery Key to continue.": "Gib deinen Wiederherstellungsschlüssel ein um fortzufahren.",
"Create a Recovery Key": "Erzeuge einen Wiederherstellungsschlüssel",
"Upgrade your Recovery Key": "Aktualisiere deinen Wiederherstellungsschlüssel",
"Store your Recovery Key": "Speichere deinen Wiederherstellungsschlüssel"
}

View file

@ -148,6 +148,7 @@
"Actions": "Actions",
"Advanced": "Advanced",
"Other": "Other",
"Command error": "Command error",
"Usage": "Usage",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
"Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown",
@ -432,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 - refresh to apply changes)": "Use the improved room list (in development - refresh to apply changes)",
"Use the improved room list (in development - will refresh to apply changes)": "Use the improved room list (in development - 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",
@ -479,7 +480,6 @@
"Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
"Enable message search in encrypted rooms": "Enable message search in encrypted rooms",
"Keep recovery passphrase in memory for this session": "Keep recovery passphrase in memory for this session",
"How fast should messages be downloaded.": "How fast should messages be downloaded.",
"Manually verify all remote sessions": "Manually verify all remote sessions",
"IRC display name width": "IRC display name width",
@ -1171,7 +1171,6 @@
"All Rooms": "All Rooms",
"Search…": "Search…",
"Server error": "Server error",
"Command error": "Command error",
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
"Unknown Command": "Unknown Command",
"Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s",
@ -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,10 @@
"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.",
"Use Recovery Passphrase or Key": "Use Recovery Passphrase or Key",
"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 +2168,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

@ -977,10 +977,10 @@
"User %(user_id)s does not exist": "El usuario %(user_id)s no existe",
"User %(user_id)s may or may not exist": "El usuario %(user_id)s podría o no existir",
"Unknown server error": "Error desconocido del servidor",
"Use a few words, avoid common phrases": "Usa unas pocas palabras, evita frases comunes",
"Use a few words, avoid common phrases": "Usa algunas palabras, evita frases comunes",
"No need for symbols, digits, or uppercase letters": "No hacen falta símbolos, números o letrás en mayúscula",
"Avoid repeated words and characters": "Evita repetir palabras y letras",
"Avoid sequences": "Evita frases",
"Avoid sequences": "Evita secuencias",
"Avoid recent years": "Evita años recientes",
"Avoid years that are associated with you": "Evita años que estén asociados contigo",
"Avoid dates and years that are associated with you": "Evita fechas y años que están asociados contigo",
@ -1176,10 +1176,10 @@
"Incoming Verification Request": "Petición de verificación entrante",
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s cambió la regla para unirse a %(rule)s",
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s cambió el acceso para invitados a %(rule)s",
"Use a longer keyboard pattern with more turns": "Usa un patrón de tecleo más largo y con más vueltas",
"Use a longer keyboard pattern with more turns": "Usa un patrón de tecleo largo con más vueltas",
"Enable Community Filter Panel": "Habilitar el Panel de Filtro de Comunidad",
"Verify this user by confirming the following emoji appear on their screen.": "Verifica este usuario confirmando que los siguientes emojis aparecen en su pantalla.",
"Your Riot is misconfigured": "Riot tiene un error de configuración",
"Your Riot is misconfigured": "Tu Riot está mal configurado",
"Whether or not you're logged in (we don't record your username)": "Hayas o no iniciado sesión (no guardamos tu nombre de usuario)",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Uses o no los 'breadcrumbs' (iconos sobre la lista de salas)",
"A conference call could not be started because the integrations server is not available": "No se pudo iniciar la conferencia porque el servidor de integraciones no está disponible",
@ -1557,7 +1557,7 @@
"Use your account or create a new one to continue.": "Usa tu cuenta existente o crea una nueva para continuar.",
"Create Account": "Crear cuenta",
"Sign In": "Registrarse",
"Sends a message as html, without interpreting it as markdown": "Envía un mensaje como html, sin interpretarlo como un markdown",
"Sends a message as html, without interpreting it as markdown": "Envía un mensaje como html, sin interpretarlo en markdown",
"Failed to set topic": "No se ha podido establecer el tema",
"Command failed": "El comando falló",
"Could not find user in room": "No pude encontrar el usuario en la sala",
@ -1747,7 +1747,7 @@
"Can't find this server or its room list": "No puedo encontrar este servidor o su lista de salas",
"All rooms": "Todas las salas",
"Your server": "Tu",
"Are you sure you want to remove <b>%(serverName)s</b>": "¿Está seguro de querer eliminar <b>%(serverName)s</b>?",
"Are you sure you want to remove <b>%(serverName)s</b>": "¿ Está seguro de querer eliminar <b>%(serverName)s</b>?",
"Remove server": "Quitar servidor",
"Matrix": "Matrix",
"Add a new server": "Añadir un nuevo servidor",
@ -2065,12 +2065,12 @@
"Unable to restore backup": "No se pudo restaurar la copia de seguridad",
"No backup found!": "¡No se encontró una copia de seguridad!",
"Keys restored": "Se restauraron las claves",
"Failed to decrypt %(failedCount)s sessions!": "¡Error en desencriptar %(failedCount) sesiones!",
"Failed to decrypt %(failedCount)s sessions!": "¡Error en descifrar %(failedCount) sesiones!",
"Successfully restored %(sessionCount)s keys": "%(sessionCount)s claves restauradas con éxito",
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Advertencia</b>: sólo debes configurar la copia de seguridad de claves desde un ordenador de su confianza.",
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Advertencia</b>: deberías configurar la copia de seguridad de claves solamente usando un ordenador de confianza.",
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Acceda a su historial de mensajes seguros y configure la mensajería segura introduciendo su contraseña de recuperación.",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "Si has olvidado tu contraseña de recuperación puedes <button1> usar tu clave de recuperación </button1> o <button2> configurar nuevas opciones de recuperación </button2>",
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Advertencia</b>: Sólo debes configurar la copia de seguridad de claves desde un ordenador de su confianza.",
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Advertencia</b>: Configurar la copia de seguridad de claves solamente usando un ordenador de confianza.",
"Access your secure message history and set up secure messaging by entering your recovery key.": "Accede a tu historial de mensajes seguros y configura la mensajería segura introduciendo tu clave de recuperación.",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "Si has olvidado tu clave de recuperación puedes <button> configurar nuevas opciones de recuperación</button>",
"Resend edit": "Reenviar la edición",
@ -2189,5 +2189,7 @@
"Identity server URL does not appear to be a valid identity server": "La URL del servidor de identidad no parece ser un servidor de identidad válido",
"General failure": "Error no especificado",
"This homeserver does not support login using email address.": "Este servidor doméstico no admite iniciar sesión con una dirección de correo electrónico.",
"This account has been deactivated.": "Esta cuenta ha sido desactivada."
"This account has been deactivated.": "Esta cuenta ha sido desactivada.",
"Room name or address": "Nombre o dirección de la sala",
"Address (optional)": "Dirección (opcional)"
}

View file

@ -1432,5 +1432,52 @@
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Vigadega seotud logid sisaldavad rakenduse teavet, sealhulgas sinu kasutajanime, külastatud jututubade kasutajatunnuseid või aliasi ning teiste kasutajate kasutajanimesid. Logides ei ole saadetud sõnumite sisu.",
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Enne logide saatmist sa peaksid <a>GitHub'is looma veateate</a> ja kirjeldama seal tekkinud probleemi.",
"GitHub issue": "Veateade GitHub'is",
"Notes": "Märkused"
"Notes": "Märkused",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s muutis uueks teemaks \"%(topic)s\".",
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s uuendas seda jututuba.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s muutis selle jututoa avalikuks kõigile, kes teavad tema aadressi.",
"sent an image.": "saatis ühe pildi.",
"Light": "Hele",
"Dark": "Tume",
"You signed in to a new session without verifying it:": "Sa logisid sisse uude sessiooni ilma seda verifitseerimata:",
"Verify your other session using one of the options below.": "Verifitseeri oma teine sessioon kasutades üht alljärgnevatest võimalustest.",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) logis sisse uude sessiooni ilma seda verifitseerimata:",
"Not Trusted": "Ei ole usaldusväärne",
"Done": "Valmis",
"%(displayName)s is typing …": "%(displayName)s kirjutab midagi…",
"%(names)s and %(count)s others are typing …|other": "%(names)s ja %(count)s muud kasutajat kirjutavad midagi…",
"%(names)s and %(count)s others are typing …|one": "%(names)s ja üks teine kasutaja kirjutavad midagi…",
"%(names)s and %(lastPerson)s are typing …": "%(names)s ja %(lastPerson)s kirjutavad midagi…",
"Cannot reach homeserver": "Koduserver ei ole hetkel leitav",
"Ensure you have a stable internet connection, or get in touch with the server admin": "Palun kontrolli, kas sul on toimiv internetiühendus ning kui on, siis küsi abi koduserveri haldajalt",
"Your Riot is misconfigured": "Sinu Riot'i seadistused on paigast ära",
"Your homeserver does not support session management.": "Sinu koduserver ei toeta sessioonide haldust.",
"Unable to load session list": "Sessioonide laadimine ei õnnestunud",
"Identity server URL does not appear to be a valid identity server": "Isikutuvastusserveri aadress ei tundu viitama kehtivale isikutuvastusserverile",
"This isn't the recovery key for your account": "See ei ole sinu konto taastevõti",
"This isn't a valid recovery key": "See ei ole nõuetekohane taastevõti",
"Looks good!": "Tundub õige!",
"Use Recovery Key or Passphrase": "Kasuta taastevõtit või paroolifraasi",
"Use Recovery Key": "Kasuta taastevõtit",
"or another cross-signing capable Matrix client": "või mõnda teist Matrix'i klienti, mis oskab risttunnustamist",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "Jätkamiseks sisesta oma taastevõti või <a>taastamise paroolifraas</a>.",
"Enter your Recovery Key to continue.": "Jätkamiseks sisesta oma taastevõti.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Sinu uus sessioon on nüüd verifitseeritud. Selles sessioonis saad lugeda oma krüptitud sõnumeid ja teiste kasutajate jaoks on ta usaldusväärne.",
"Your new session is now verified. Other users will see it as trusted.": "Sinu uus sessioon on nüüd verifitseeritud. Teiste kasutajate jaoks on ta usaldusväärne.",
"Without completing security on this session, it wont have access to encrypted messages.": "Kui sa pole selle sessiooni turvaprotseduure lõpetanud, siis sul puudub ligipääs oma krüptitud sõnumitele.",
"Go Back": "Mine tagasi",
"Failed to re-authenticate due to a homeserver problem": "Uuesti autentimine ei õnnestunud koduserveri vea tõttu",
"Incorrect password": "Vale salasõna",
"Failed to re-authenticate": "Uuesti autentimine ei õnnestunud",
"Regain access to your account and recover encryption keys stored in this session. Without them, you wont be able to read all of your secure messages in any session.": "Taasta ligipääs oma kontole ning selles sessioonis salvestatud krüptivõtmetele. Ilma nende võtmeteta sa ei saa lugeda krüptitud sõnumeid mitte üheski oma sessioonis.",
"Command Autocomplete": "Käskude automaatne lõpetamine",
"Community Autocomplete": "Kogukondade nimede automaatne lõpetamine",
"Emoji Autocomplete": "Emoji'de automaatne lõpetamine",
"Notification Autocomplete": "Teavituste automaatne lõpetamine",
"Room Autocomplete": "Jututubade nimede automaatne lõpetamine",
"User Autocomplete": "Kasutajanimede automaatne lõpetamine",
"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"
}

View file

@ -2340,5 +2340,32 @@
"This address is available to use": "Tämä osoite on käytettävissä",
"This address is already in use": "Tämä osoite on jo käytössä",
"Set a room address to easily share your room with other people.": "Aseta huoneelle osoite, jotta voit jakaa huoneen helposti muille.",
"Address (optional)": "Osoite (valinnainen)"
"Address (optional)": "Osoite (valinnainen)",
"sent an image.": "lähetti kuvan.",
"Light": "Vaalea",
"Dark": "Tumma",
"You: %(message)s": "Sinä: %(message)s",
"Emoji picker": "Emojivalitsin",
"No recently visited rooms": "Ei hiljattain vierailtuja huoneita",
"People": "Ihmiset",
"Sort by": "Lajittelutapa",
"Unread rooms": "Lukemattomat huoneet",
"Always show first": "Näytä aina ensin",
"Show": "Näytä",
"Leave Room": "Poistu huoneesta",
"Switch to light mode": "Vaihda vaaleaan teemaan",
"Switch to dark mode": "Vaihda tummaan teemaan",
"Switch theme": "Vaihda teemaa",
"All settings": "Kaikki asetukset",
"Archived rooms": "Arkistoidut huoneet",
"Feedback": "Palaute",
"Account settings": "Tilin asetukset",
"Recovery Key": "Palautusavain",
"This isn't the recovery key for your account": "Tämä ei ole tilisi palautusavain",
"This isn't a valid recovery key": "Tämä ei ole kelvollinen palautusavain",
"Looks good!": "Hyvältä näyttää!",
"Use Recovery Key or Passphrase": "Käytä palautusavainta tai salalausetta",
"Use Recovery Key": "Käytä palautusavainta",
"Create a Recovery Key": "Luo palautusavain",
"Upgrade your Recovery Key": "Päivitä palautusavaimesi"
}

View file

@ -2508,5 +2508,23 @@
"Leave Room": "Quitter le salon",
"Room options": "Options du salon",
"Activity": "Activité",
"A-Z": "A-Z"
"A-Z": "A-Z",
"Light": "Clair",
"Dark": "Sombre",
"Customise your appearance": "Personnalisez lapparence",
"Appearance Settings only affect this Riot session.": "Les paramètres dapparence affecteront uniquement cette session de Riot.",
"Recovery Key": "Clé de récupération",
"This isn't the recovery key for your account": "Ce nest pas la clé de récupération pour votre compte",
"This isn't a valid recovery key": "Ce nest pas une clé de récupération valide",
"Looks good!": "Ça a lair correct !",
"Use Recovery Key or Passphrase": "Utiliser la clé ou la phrase secrète de récupération",
"Use Recovery Key": "Utiliser la clé de récupération",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "Saisissez votre clé de récupération ou une <a>phrase secrète de récupération</a> pour continuer.",
"Enter your Recovery Key to continue.": "Saisissez votre clé de récupération pour continuer.",
"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.": "Mettez à niveau votre clé de récupération pour stocker vos clés 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.",
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Stockez votre clé de récupération dans un endroit sûr, elle peut être utilisée pour déverrouiller vos messages et vos données chiffrés.",
"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"
}

View file

@ -789,7 +789,7 @@
"Failed to set Direct Message status of room": "Fallo ao establecer o estado Mensaxe Directa da sala",
"Monday": "Luns",
"Remove from Directory": "Eliminar do directorio",
"Enable them now": "Activalos agora",
"Enable them now": "Activalas agora",
"Toolbox": "Ferramentas",
"Collecting logs": "Obtendo rexistros",
"You must specify an event type!": "Debe indicar un tipo de evento!",
@ -1660,5 +1660,125 @@
"Everyone in this room is verified": "Todas nesta sala están verificadas",
"Edit message": "Editar mensaxe",
"Mod": "Mod",
"Your key share request has been sent - please check your other sessions for key share requests.": "Enviouse a solicitude de compartir chave - comproba as túas outras sesións para solicitudes de compartir chave."
"Your key share request has been sent - please check your other sessions for key share requests.": "Enviouse a solicitude de compartir chave - comproba as túas outras sesións para solicitudes de compartir chave.",
"Light": "Claro",
"Dark": "Escuro",
"Customise your appearance": "Personaliza o aspecto",
"Appearance Settings only affect this Riot session.": "Os axustes da aparencia só lle afectan a esta sesión Riot.",
"Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "As solicitudes de compartir Chave envíanse ás outras túas sesións abertas. Se rexeitaches ou obviaches a solicitude nas outras sesións, preme aquí para voltar a facer a solicitude.",
"If your other sessions do not have the key for this message you will not be able to decrypt them.": "Se as túas outras sesións non teñen a chave para esta mensaxe non poderás descifrala.",
"<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Volta a solicitar chaves de cifrado</requestLink> desde as outras sesións.",
"This message cannot be decrypted": "Esta mensaxe non pode descifrarse",
"Encrypted by an unverified session": "Cifrada por unha sesión non verificada",
"Unencrypted": "Non cifrada",
"Encrypted by a deleted session": "Cifrada por unha sesión eliminada",
"Invite only": "Só por convite",
"Scroll to most recent messages": "Ir ás mensaxes máis recentes",
"Close preview": "Pechar vista previa",
"Emoji picker": "Selector Emoticona",
"Send a reply…": "Responder…",
"Send a message…": "Enviar mensaxe…",
"The conversation continues here.": "A conversa continúa aquí.",
"This room has been replaced and is no longer active.": "Esta sala foi substituída e xa non está activa.",
"Bold": "Resaltado",
"Italics": "Cursiva",
"Code block": "Bloque de código",
"No recently visited rooms": "Sen salas recentes visitadas",
"People": "Persoas",
"Joining room …": "Uníndote a sala…",
"Loading …": "Cargando…",
"Rejecting invite …": "Rexeitando convite…",
"Loading room preview": "Cargando vista previa",
"You were kicked from %(roomName)s by %(memberName)s": "Foches expulsada de %(roomName)s por %(memberName)s",
"Reason: %(reason)s": "Razón: %(reason)s",
"Forget this room": "Esquecer sala",
"You were banned from %(roomName)s by %(memberName)s": "Foches bloqueada en %(roomName)s por %(memberName)s",
"Something went wrong with your invite to %(roomName)s": "Algo fallou co teu convite para %(roomName)s",
"An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Un erro (%(errcode)s) foi devolto ao intentar validar o convite. Podes intentar enviarlle esta información a administración da sala.",
"This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Este convite para %(roomName)s foi enviado a %(email)s que non está asociado coa túa conta",
"Link this email with your account in Settings to receive invites directly in Riot.": "Liga este email coa túa conta nos Axustes para recibir convites directamente en Riot.",
"This invite to %(roomName)s was sent to %(email)s": "Este convite para %(roomName)s foi enviado a %(email)s",
"Use an identity server in Settings to receive invites directly in Riot.": "Usa un servidor de identidade nos Axustes para recibir convites directamente en Riot.",
"Share this email in Settings to receive invites directly in Riot.": "Comparte este email en Axustes para recibir convites directamente en Riot.",
"Do you want to chat with %(user)s?": "Desexas conversar con %(user)s?",
"<userName/> wants to chat": "<userName/> quere conversar",
"Start chatting": "Comeza a conversa",
"<userName/> invited you": "<userName/> convidoute",
"Reject & Ignore user": "Rexeitar e Ignorar usuaria",
"This room doesn't exist. Are you sure you're at the right place?": "Esta sala non existe. ¿Tes a certeza de estar no lugar correcto?",
"Try again later, or ask a room admin to check if you have access.": "Inténtao máis tarde, ou pídelle á administración da instancia que comprobe se tes acceso.",
"%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s foi devolto ao intentar acceder a sala. Se cres que esta mensaxe non é correcta, por favor <issueLink>envía un informe de fallo</issueLink>.",
"Never lose encrypted messages": "Non perdas nunca acceso ás mensaxes cifradas",
"Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "As mensaxes nesta sala están aseguradas con cifrado extremo-a-extremo. Só ti e o correspondente(s) tedes as chaves para ler as mensaxes.",
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Fai unha copia das chaves para evitar perdelas. <a>Saber máis.</a>",
"Not now": "Agora non",
"Don't ask me again": "Non preguntarme outra vez",
"Sort by": "Orde por",
"Activity": "Actividade",
"A-Z": "A-Z",
"Unread rooms": "",
"Always show first": "Mostrar sempre primeiro",
"Show": "Mostrar",
"Message preview": "Vista previa da mensaxe",
"List options": "Opcións da listaxe",
"Add room": "Engadir sala",
"Show %(count)s more|other": "Mostrar %(count)s máis",
"Show %(count)s more|one": "Mostrar %(count)s máis",
"%(count)s unread messages including mentions.|other": "%(count)s mensaxes non lidas incluíndo mencións.",
"%(count)s unread messages including mentions.|one": "1 mención non lida.",
"%(count)s unread messages.|other": "%(count)s mensaxe non lidas.",
"%(count)s unread messages.|one": "1 mensaxe non lida.",
"Unread mentions.": "Mencións non lidas.",
"Unread messages.": "Mensaxes non lidas.",
"Leave Room": "Deixar a Sala",
"Room options": "Opcións da Sala",
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Ao actualizar a sala pecharás a instancia actual da sala e crearás unha versión mellorada co mesmo nome.",
"This room has already been upgraded.": "Esta sala xa foi actualizada.",
"This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "A sala está usando a versión <roomVersion />, que este servidor considera como <i>non estable</i>.",
"Only room administrators will see this warning": "Só a administración da sala pode ver este aviso",
"Unknown Command": "Comando descoñecido",
"Unrecognised command: %(commandText)s": "Comando non recoñecido: %(commandText)s",
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Truco: Comeza a mensaxe con <code>//</code> para comezar cun trazo.",
"Send as message": "Enviar como mensaxe",
"Failed to connect to integration manager": "Fallou a conexión co xestor de integracións",
"Add some now": "Engade algún agora",
"Failed to revoke invite": "Fallo ao revogar o convite",
"Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Non se revogou o convite. O servidor podería estar experimentando un problema temporal ou non tes permisos suficientes para revogar o convite.",
"Revoke invite": "Revogar convite",
"Invited by %(sender)s": "Convidada por %(sender)s",
"Mark all as read": "Marcar todo como lido",
"Error updating main address": "Fallo ao actualizar o enderezo principal",
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Algo fallou ao actualizar o enderezo principal da sala. Podería non estar autorizado polo servidor ou ser un fallo temporal.",
"Error creating address": "Fallo ao crear o enderezo",
"There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Algo fallou ao crear ese enderezo. Podería non estar autorizado polo servidor ou ser un fallo temporal.",
"You don't have permission to delete the address.": "Non tes permiso para eliminar o enderezo.",
"There was an error removing that address. It may no longer exist or a temporary error occurred.": "Houbo un erro ao eliminar o enderezo. Podería non existir ou ser un fallo temporal.",
"Error removing address": "Erro ao eliminar o enderezo",
"Main address": "Enderezo principal",
"Local address": "Enderezo local",
"Published Addresses": "Enderezos publicados",
"Other published addresses:": "Outros enderezos publicados:",
"No other published addresses yet, add one below": "Aínda non hai outros enderezos publicados, engade un embaixo",
"New published address (e.g. #alias:server)": "Novo enderezo publicado (ex. #alias:servidor)",
"Local Addresses": "Enderezos locais",
"Room Name": "Nome da sala",
"Room Topic": "Asunto da sala",
"Room avatar": "Avatar da sala",
"Waiting for you to accept on your other session…": "Agardando a que aceptes na túa outra sesión…",
"Waiting for %(displayName)s to accept…": "Agardando a que %(displayName)s acepte…",
"Accepting…": "Aceptando…",
"Start Verification": "Comezar a Verificación",
"Your messages are secured and only you and the recipient have the unique keys to unlock them.": "As túas mensaxes están seguras e só ti e o correspondente tedes as únicas chaves que as desbloquean.",
"In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "Nas salas cifradas, as túas mensaxes están seguras e só ti e o correspondente tedes as únicas chaves que as desbloquean.",
"Verify User": "Verificar Usuaria",
"For extra security, verify this user by checking a one-time code on both of your devices.": "Para maior seguridade, verifica esta usuaria comprobando o código temporal en dous dos teus dispositivos.",
"Your messages are not secure": "As túas mensaxes non están aseguradas",
"One of the following may be compromised:": "Un dos seguintes podería estar comprometido:",
"Your homeserver": "O teu servidor",
"The homeserver the user youre verifying is connected to": "O servidor ao que a usuaria que estás a verificar está conectada",
"Yours, or the other users internet connection": "A túa, ou a conexión a internet da outra usuaria",
"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"
}

View file

@ -2494,5 +2494,25 @@
"All settings": "Minden beállítás",
"Archived rooms": "Archivált szobák",
"Feedback": "Visszajelzés",
"Account settings": "Fiók beállítása"
"Account settings": "Fiók beállítása",
"Light": "Világos",
"Dark": "Sötét",
"Customise your appearance": "Szabd személyre a megjelenítést",
"Appearance Settings only affect this Riot session.": "A megjelenítési beállítások csak erre a munkamenetre vonatkoznak.",
"Activity": "Aktivitás",
"A-Z": "A-Z",
"Recovery Key": "Visszaállítási Kulcs",
"This isn't the recovery key for your account": "Ez nem a fiókod visszaállítási kulcsa",
"This isn't a valid recovery key": "Ez nem egy érvényes visszaállítási kulcs",
"Looks good!": "Jól néz ki!",
"Use Recovery Key or Passphrase": "Használd a visszaállítási kulcsot vagy jelmondatot",
"Use Recovery Key": "Használd a Visszaállítási Kulcsot",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "A továbblépéshez add meg a Visszaállítási kulcsot vagy a <a>Visszaállítási jelmondatot</a>.",
"Enter your Recovery Key to continue.": "A továbblépéshez add meg a Visszaállítási Kulcsot.",
"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.": "Fejleszd a 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.",
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "A Visszaállítási kulcsot tárold biztonságos helyen mivel felhasználható a titkosított üzenetekhez és adatokhoz való hozzáféréshez.",
"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"
}

View file

@ -2502,5 +2502,15 @@
"Leave Room": "Lascia stanza",
"Room options": "Opzioni stanza",
"Activity": "Attività",
"A-Z": "A-Z"
"A-Z": "A-Z",
"Light": "Chiaro",
"Dark": "Scuro",
"Customise your appearance": "Personalizza l'aspetto",
"Appearance Settings only affect this Riot session.": "Le impostazioni dell'aspetto hanno effetto solo in questa sessione di Riot.",
"Recovery Key": "Chiave di recupero",
"This isn't the recovery key for your account": "Questa non è la chiave di recupero del tuo account",
"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"
}

View file

@ -2211,12 +2211,18 @@
"Are you sure you want to deactivate your account? This is irreversible.": "Weet u zeker dat u uw account wil sluiten? Dit is onomkeerbaar.",
"Confirm account deactivation": "Bevestig accountsluiting",
"Room name or address": "Gespreksnaam of -adres",
"Joins room with given address": "Treedt tot het gesprek met het opgegeven adres toe",
"Joins room with given address": "Neem aan het gesprek met dat adres deel",
"Unrecognised room address:": "Gespreksadres niet herkend:",
"Help us improve Riot": "Help ons Riot nog beter te maken",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Stuur <UsageDataLink>anonieme gebruiksinformatie</UsageDataLink> waarmee we Riot kunnen verbeteren. Dit plaatst een <PolicyLink>cookie</PolicyLink>.",
"I want to help": "Ik wil helpen",
"Your homeserver has exceeded its user limit.": "Uw thuisserver heeft het maximaal aantal gebruikers overschreden.",
"Your homeserver has exceeded one of its resource limits.": "Uw thuisserver heeft een van zijn limieten overschreden.",
"Ok": "Oké"
"Ok": "Oké",
"sent an image.": "heeft een plaatje gestuurd.",
"Light": "Helder",
"Dark": "Donker",
"Set password": "Stel wachtwoord in",
"To return to your account in future you need to set a password": "Zonder wachtwoord kunt u later niet tot uw account terugkeren",
"Restart": "Herstarten"
}

View file

@ -157,7 +157,7 @@
"Emoji": "Emoji",
"Enable automatic language detection for syntax highlighting": "Włącz automatyczne rozpoznawanie języka dla podświetlania składni",
"Enable Notifications": "Włącz powiadomienia",
"%(senderName)s ended the call.": "%(senderName)s zakończył połączenie.",
"%(senderName)s ended the call.": "%(senderName)s zakończył(a) połączenie.",
"End-to-end encryption information": "Informacje o szyfrowaniu end-to-end",
"Enter passphrase": "Wpisz frazę",
"Error decrypting attachment": "Błąd odszyfrowywania załącznika",
@ -230,8 +230,8 @@
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla wszyscy członkowie pokoju, od momentu ich zaproszenia.",
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla wszyscy członkowie pokoju, od momentu ich dołączenia.",
"%(senderName)s made future room history visible to all room members.": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla wszyscy członkowie pokoju.",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s uczynił przyszłą historię pokoju widoczną dla kazdego.",
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s uczynił przyszłą historię pokoju widoczną dla nieznany (%(visibility)s).",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla każdego.",
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla nieznany (%(visibility)s).",
"Manage Integrations": "Zarządzaj integracjami",
"Missing room_id in request": "Brakujące room_id w żądaniu",
"Missing user_id in request": "Brakujące user_id w żądaniu",
@ -291,7 +291,7 @@
"Send anyway": "Wyślij mimo to",
"Send Reset Email": "Wyślij e-mail resetujący hasło",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s wysłał obraz.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s wysłał zaproszenie do %(targetDisplayName)s do dołączenia do pokoju.",
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s wysłał(a) zaproszenie do %(targetDisplayName)s do dołączenia do pokoju.",
"Server error": "Błąd serwera",
"Server may be unavailable, overloaded, or search timed out :(": "Serwer może być niedostępny, przeciążony, lub upłynął czas wyszukiwania :(",
"Server may be unavailable, overloaded, or you hit a bug.": "Serwer może być niedostępny, przeciążony, lub trafiłeś na błąd.",
@ -363,7 +363,7 @@
"You do not have permission to do that in this room.": "Nie masz pozwolenia na wykonanie tej akcji w tym pokoju.",
"You cannot place a call with yourself.": "Nie możesz wykonać połączenia do siebie.",
"You cannot place VoIP calls in this browser.": "Nie możesz przeprowadzić rozmowy głosowej VoIP w tej przeglądarce.",
"You do not have permission to post to this room": "Nie jesteś uprawniony do pisania w tym pokoju",
"You do not have permission to post to this room": "Nie masz uprawnień do pisania w tym pokoju",
"You have <a>disabled</a> URL previews by default.": "Masz domyślnie <a>wyłączone</a> podglądy linków.",
"You have no visible notifications": "Nie masz widocznych powiadomień",
"You must <a>register</a> to use this functionality": "Musisz się <a>zarejestrować</a> aby móc używać tej funkcji",
@ -915,7 +915,7 @@
"Common names and surnames are easy to guess": "Popularne imiona i nazwiska są łatwe do odgadnięcia",
"You do not have permission to invite people to this room.": "Nie masz uprawnień do zapraszania ludzi do tego pokoju.",
"User %(user_id)s does not exist": "Użytkownik %(user_id)s nie istnieje",
"Unknown server error": "Nieznany bład serwera",
"Unknown server error": "Nieznany błąd serwera",
"%(oneUser)sleft %(count)s times|one": "%(oneUser)swyszedł(-ła)",
"%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sdołączył(a) i wyszedł(-ła) %(count)s razy",
"%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sdołączył(a) i wyszedł(-ła)",
@ -930,7 +930,7 @@
"%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)szmienił(a) swój awatar %(count)s razy",
"%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)szmienił(a) swój awatar",
"%(items)s and %(count)s others|other": "%(items)s i %(count)s innych",
"%(items)s and %(count)s others|one": "%(items)s i jeden inny",
"%(items)s and %(count)s others|one": "%(items)s i jedna inna osoba",
"%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)szmieniło swój awatar",
"%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)szmieniło swój awatar %(count)s razy",
"%(oneUser)schanged their name %(count)s times|one": "%(oneUser)szmienił(a) swoją nazwę",
@ -1102,7 +1102,7 @@
"Gift": "Prezent",
"Hammer": "Młotek",
"Group & filter rooms by custom tags (refresh to apply changes)": "Grupuj i filtruj pokoje według niestandardowych znaczników (odśwież, aby zastosować zmiany)",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Poprzedza ¯\\_(ツ)_/¯ do wiadomości tekstowej",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Dodaje ¯\\_(ツ)_/¯ na początku wiadomości tekstowej",
"Upgrades a room to a new version": "Aktualizuje pokój do nowej wersji",
"Changes your display nickname in the current room only": "Zmienia Twój wyświetlany pseudonim tylko dla bieżącego pokoju",
"Sets the room name": "Ustawia nazwę pokoju",
@ -1175,7 +1175,7 @@
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s aktywował Flair dla %(groups)s w tym pokoju.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s dezaktywował Flair dla %(groups)s w tym pokoju.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s aktywował Flair dla %(newGroups)s i dezaktywował Flair dla %(oldGroups)s w tym pokoju.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s odwołał zaproszednie dla %(targetDisplayName)s aby dołączył do pokoju.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s odwołał(a) zaproszenie dla %(targetDisplayName)s aby dołączył do pokoju.",
"%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze…",
"Cannot reach homeserver": "Błąd połączenia z serwerem domowym",
"Ensure you have a stable internet connection, or get in touch with the server admin": "Upewnij się, że posiadasz stabilne połączenie internetowe lub skontaktuj się z administratorem serwera",
@ -1605,5 +1605,9 @@
"WARNING: Session already verified, but keys do NOT MATCH!": "OSTRZEŻENIE: Sesja już zweryfikowana, ale klucze do siebie NIE PASUJĄ!",
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "OSTRZEŻENIE: WERYFIKACJA KLUCZY NIE POWIODŁA SIĘ! Klucz podpisujący dla %(userId)s oraz sesji %(deviceId)s to \"%(fprint)s\", nie pasuje on do podanego klucza \"%(fingerprint)s\". To może oznaczać że Twoja komunikacja jest przechwytywana!",
"Displays information about a user": "Pokazuje informacje na temat użytkownika",
"Send a bug report with logs": "Wyślij raport błędu z logami"
"Send a bug report with logs": "Wyślij raport błędu z logami",
"Use Single Sign On to continue": "Użyj pojedynczego logowania, aby kontynuować",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Potwierdź dodanie tego adresu e-mail przez użycie pojedynczego logowania, aby potwierdzić swoją tożsamość.",
"Single Sign On": "Pojedyncze logowanie",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Potwierdź dodanie tego numeru telefonu przez użycie pojedynczego logowania, aby potwierdzić swoją tożsamość."
}

View file

@ -97,13 +97,13 @@
"Who can read history?": "Кто может читать историю?",
"You do not have permission to post to this room": "Вы не можете писать в эту комнату",
"You have no visible notifications": "Нет видимых уведомлений",
"%(targetName)s accepted an invitation.": "%(targetName)s принимает приглашение.",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s принимает приглашение от %(displayName)s.",
"%(targetName)s accepted an invitation.": "%(targetName)s принял приглашение.",
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s принял приглашение от %(displayName)s.",
"Active call": "Активный вызов",
"%(senderName)s answered the call.": "%(senderName)s ответил(а) на звонок.",
"%(senderName)s banned %(targetName)s.": "%(senderName)s забанил(а) %(targetName)s.",
"Call Timeout": "Нет ответа",
"%(senderName)s changed their profile picture.": "%(senderName)s изменяет свой аватар.",
"%(senderName)s changed their profile picture.": "%(senderName)s изменил свой аватар.",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s изменил(а) уровни прав %(powerLevelDiffText)s.",
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s изменил(а) название комнаты на %(roomName)s.",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s изменил(а) тему комнаты на \"%(topic)s\".",
@ -115,8 +115,8 @@
"Failure to create room": "Не удалось создать комнату",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "для %(userId)s с %(fromPowerLevel)s на %(toPowerLevel)s",
"click to reveal": "нажмите для открытия",
"%(senderName)s invited %(targetName)s.": "%(senderName)s приглашает %(targetName)s.",
"%(targetName)s joined the room.": "%(targetName)s входит в комнату.",
"%(senderName)s invited %(targetName)s.": "%(senderName)s пригласил %(targetName)s.",
"%(targetName)s joined the room.": "%(targetName)s вошёл в комнату.",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s выгнал(а) %(targetName)s.",
"%(targetName)s left the room.": "%(targetName)s покидает комнату.",
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s сделал(а) историю разговора видимой для всех собеседников с момента их приглашения.",
@ -223,9 +223,9 @@
"Reason": "Причина",
"%(targetName)s rejected the invitation.": "%(targetName)s отклонил(а) приглашение.",
"Reject invitation": "Отклонить приглашение",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s удаляет своё отображаемое имя (%(oldDisplayName)s).",
"%(senderName)s removed their profile picture.": "%(senderName)s удаляет свой аватар.",
"%(senderName)s requested a VoIP conference.": "%(senderName)s хочет начать конференц-звонок.",
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s удалил своё отображаемое имя (%(oldDisplayName)s).",
"%(senderName)s removed their profile picture.": "%(senderName)s удалил свой аватар.",
"%(senderName)s requested a VoIP conference.": "%(senderName)s запросил конференц-звонок.",
"Riot does not have permission to send you notifications - please check your browser settings": "У Riot нет разрешения на отправку уведомлений — проверьте настройки браузера",
"Riot was not given permission to send notifications - please try again": "Riot не получил разрешение на отправку уведомлений, пожалуйста, попробуйте снова",
"riot-web version:": "версия riot-web:",
@ -302,8 +302,8 @@
"Server may be unavailable, overloaded, or you hit a bug.": "Возможно, сервер недоступен, перегружен или случилась ошибка.",
"Server unavailable, overloaded, or something else went wrong.": "Возможно, сервер недоступен, перегружен или что-то еще пошло не так.",
"Session ID": "ID сессии",
"%(senderName)s set a profile picture.": "%(senderName)s устанавливает себе аватар.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s меняет отображаемое имя на %(displayName)s.",
"%(senderName)s set a profile picture.": "%(senderName)s установил себе аватар.",
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s изменил отображаемое имя на %(displayName)s.",
"Signed Out": "Выполнен выход",
"This room is not accessible by remote Matrix servers": "Это комната недоступна из других серверов Matrix",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Попытка загрузить выбранный интервал истории чата этой комнаты не удалась, так как у вас нет разрешений на просмотр.",
@ -696,7 +696,7 @@
"This room is not public. You will not be able to rejoin without an invite.": "Эта комната не является публичной. Вы не сможете войти без приглашения.",
"Community IDs cannot be empty.": "ID сообществ не могут быть пустыми.",
"<a>In reply to</a> <pill>": "<a>В ответ на</a> <pill>",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s меняет отображаемое имя на %(displayName)s.",
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s изменил отображаемое имя на %(displayName)s.",
"Failed to set direct chat tag": "Не удалось установить тег прямого чата",
"Failed to remove tag %(tagName)s from room": "Не удалось удалить тег %(tagName)s из комнаты",
"Failed to add tag %(tagName)s to room": "Не удалось добавить тег %(tagName)s в комнату",

View file

@ -2496,5 +2496,26 @@
"Show %(count)s more|other": "Shfaq %(count)s të tjera",
"Show %(count)s more|one": "Shfaq %(count)s tjetër",
"Leave Room": "Dil Nga Dhoma",
"Room options": "Mundësi dhome"
"Room options": "Mundësi dhome",
"Light": "E çelët",
"Dark": "E errët",
"Use the improved room list (in development - will refresh to apply changes)": "Përdorni listën e përmirësuar të dhomave (në zhvillim - që të aplikohen ndryshimet, duhet rifreskuar)",
"Customise your appearance": "Përshtatni dukjen tuaj",
"Appearance Settings only affect this Riot session.": "Rregullimet e Dukjes prekin vetëm këtë sesion Riot.",
"Activity": "Veprimtari",
"A-Z": "A-Z",
"Recovery Key": "Kyç Rimarrjesh",
"This isn't the recovery key for your account": "Ky sështë kyçi i rimarrjeve për llogarinë tuaj",
"This isn't a valid recovery key": "Ky sështë kyç rimarrjesh i vlefshëm",
"Looks good!": "Mirë duket!",
"Use Recovery Key or Passphrase": "Përdorni Kyç ose Frazëkalim Rimarrjesh",
"Use Recovery Key": "Përdorni Kyç Rimarrjesh",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "Që të vazhdohet, jepni Kyçin tuaj të Rimarrjeve ose jepni një <a>Frazëkalim Rimarrjesh</a>.",
"Enter your Recovery Key to continue.": "Që të vazhdohet, jepni Kyçin tuaj të Rimarrjeve.",
"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.": "Që të depozitoni kyçe & të fshehta fshehtëzimi me të dhënat e llogarisë tuaj, përmirësoni Kyçin tuaj të Rimarrjeve. Nëse humbni këto kredenciale hyrjesh, do tju duhet të shkyçni të dhënat tuaja.",
"Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Ruajeni diku të parrezik Kyçin tuaj të Rimarrjeve, mund të përdoret për të shkyçur mesazhet & të dhënat tuaja të fshehtëzuara.",
"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.": "Krijoni një Kyç Rimarrjesh që të depozitoni kyçe & të fshehta fshehtëzimi me të dhënat e llogarisë tuaj. Nëse humbni këto kredenciale, do tju duhet të shkyçni të dhënat tuaja.",
"Create a Recovery Key": "Krijoni një Kyç Rimarrjesh",
"Upgrade your Recovery Key": "Përmirësoni Kyçin tuaj të Rimarrjeve",
"Store your Recovery Key": "Depozitoni Kyçin tuaj të Rimarrjeve"
}

View file

@ -2507,5 +2507,23 @@
"sent an image.": "傳送圖片。",
"You: %(message)s": "您:%(message)s",
"Activity": "活動",
"A-Z": "A-Z"
"A-Z": "A-Z",
"Light": "淺色",
"Dark": "暗色",
"Customise your appearance": "自訂您的外觀",
"Appearance Settings only affect this Riot session.": "外觀設定僅會影響此 Riot 工作階段。",
"Recovery Key": "復原金鑰",
"This isn't the recovery key for your account": "這不是您帳號的復原金鑰",
"This isn't a valid recovery key": "這不是有效的復原金鑰",
"Looks good!": "看起來不錯!",
"Use Recovery Key or Passphrase": "使用復原金鑰或通關密語",
"Use Recovery Key": "使用復原金鑰",
"Enter your Recovery Key or enter a <a>Recovery Passphrase</a> to continue.": "輸入您的復原金鑰或輸入<a>復原通關密語</a>以繼續。",
"Enter your Recovery Key to continue.": "輸入您的復原金鑰以繼續。",
"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.": "將復原金鑰存放在安全的地方,它可以用於解鎖您的已加密訊息與資料。",
"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": "儲存您的復原金鑰"
}

View file

@ -140,9 +140,10 @@ export const SETTINGS = {
},
"feature_new_room_list": {
isFeature: true,
displayName: _td("Use the improved room list (in development - refresh to apply changes)"),
displayName: _td("Use the improved room list (in development - will refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE,
default: false,
controller: new ReloadOnChangeController(),
},
"feature_custom_themes": {
isFeature: true,
@ -520,11 +521,6 @@ export const SETTINGS = {
displayName: _td("Enable message search in encrypted rooms"),
default: true,
},
"keepSecretStoragePassphraseForSession": {
supportedLevels: ['device', 'config'],
displayName: _td("Keep recovery passphrase in memory for this session"),
default: false,
},
"crawlerSleepTime": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("How fast should messages be downloaded."),

View file

@ -20,18 +20,20 @@ import {MatrixClientPeg} from '../../MatrixClientPeg';
// XXX: This feels wrong.
import {PushProcessor} from "matrix-js-sdk/src/pushprocessor";
function isMasterRuleEnabled() {
// .m.rule.master being enabled means all events match that push rule
// default action on this rule is dont_notify, but it could be something else
function isPushNotifyDisabled() {
// Return the value of the master push rule as a default
const processor = new PushProcessor(MatrixClientPeg.get());
const masterRule = processor.getPushRuleById(".m.rule.master");
if (!masterRule) {
console.warn("No master push rule! Notifications are disabled for this user.");
return false;
return true;
}
// Why enabled == false means "enabled" is beyond me.
return !masterRule.enabled;
// If the rule is enabled then check it does not notify on everything
return masterRule.enabled && !masterRule.actions.includes("notify");
}
function getNotifier() {
@ -45,7 +47,7 @@ export class NotificationsEnabledController extends SettingController {
if (!getNotifier().isPossible()) return false;
if (calculatedValue === null || calculatedAtLevel === "default") {
return isMasterRuleEnabled();
return !isPushNotifyDisabled();
}
return calculatedValue;
@ -63,7 +65,7 @@ export class NotificationBodyEnabledController extends SettingController {
if (!getNotifier().isPossible()) return false;
if (calculatedValue === null) {
return isMasterRuleEnabled();
return !isPushNotifyDisabled();
}
return calculatedValue;

View file

@ -39,7 +39,7 @@ export default class LocalEchoWrapper extends SettingsHandler {
const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
const bySetting = this._cache[settingName];
if (bySetting && bySetting.hasOwnProperty(cacheRoomId)) {
return bySetting[roomId];
return bySetting[cacheRoomId];
}
return this._handler.getValue(settingName, roomId);

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() {
@ -37,19 +36,11 @@ export class SetupEncryptionStore extends EventEmitter {
return;
}
this._started = true;
this.phase = PHASE_BUSY;
this.phase = PHASE_INTRO;
this.verificationRequest = null;
this.backupInfo = null;
// ID of the key that the secrets we want are encrypted with
this.keyId = null;
// 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);
this.fetchKeyInfo();
}
stop() {
@ -66,49 +57,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) {
this.keyId = null;
this.keyInfo = null;
} else {
// If the secret is stored under more than one key, we just pick an arbitrary one
this.keyId = Object.keys(keys)[0];
this.keyInfo = keys[this.keyId];
}
this.phase = PHASE_INTRO;
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 +84,6 @@ export class SetupEncryptionStore extends EventEmitter {
// to advance before this.
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
}
}, {
withKeys,
passphraseOnly: true,
}).catch(reject);
} catch (e) {
console.error(e);

View file

@ -21,11 +21,13 @@ const TILE_HEIGHT_PX = 44;
interface ISerializedListLayout {
numTiles: number;
showPreviews: boolean;
collapsed: boolean;
}
export class ListLayout {
private _n = 0;
private _previews = false;
private _collapsed = false;
constructor(public readonly tagId: TagID) {
const serialized = localStorage.getItem(this.key);
@ -34,9 +36,19 @@ export class ListLayout {
const parsed = <ISerializedListLayout>JSON.parse(serialized);
this._n = parsed.numTiles;
this._previews = parsed.showPreviews;
this._collapsed = parsed.collapsed;
}
}
public get isCollapsed(): boolean {
return this._collapsed;
}
public set isCollapsed(v: boolean) {
this._collapsed = v;
this.save();
}
public get showPreviews(): boolean {
return this._previews;
}
@ -80,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 {
@ -100,6 +116,7 @@ export class ListLayout {
return {
numTiles: this.visibleTiles,
showPreviews: this.showPreviews,
collapsed: this.isCollapsed,
};
}
}

View file

@ -181,6 +181,12 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
const room = this.matrixClient.getRoom(roomId);
const tryUpdate = async (updatedRoom: Room) => {
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`);
if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
console.log(`[RoomListDebug] Got tombstone event - regenerating room list`);
// TODO: We could probably be smarter about this
await this.regenerateAllLists();
return; // don't pass the update down - we will have already handled it in the regen
}
await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline);
};
if (!room) {
@ -334,7 +340,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
}
await this.algorithm.populateTags(sorts, orders);
await this.algorithm.setKnownRooms(this.matrixClient.getRooms());
await this.algorithm.setKnownRooms(this.matrixClient.getVisibleRooms());
this.initialListsGenerated = true;

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
@ -230,7 +230,10 @@ export class Algorithm extends EventEmitter {
// Cheaply clone the rooms so we can more easily do operations on the list.
// We optimize our lookups by trying to reduce sample size as much as possible
// to the rooms we know will be deduped by the Set.
const rooms = this.cachedRooms[tagId];
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
if (this._stickyRoom && this._stickyRoom.tag === tagId && this._stickyRoom.room) {
rooms.push(this._stickyRoom.room);
}
let remainingRooms = rooms.map(r => r);
let allowedRoomsInThisTag = [];
let lastFilterPriority = orderedFilters[0].relativePriority;
@ -505,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];
@ -538,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

@ -75,7 +75,7 @@ export class RecentAlgorithm implements IAlgorithm {
};
return rooms.sort((a, b) => {
return getLastTs(a) - getLastTs(b);
return getLastTs(b) - getLastTs(a);
});
}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room";
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
import { EventEmitter } from "events";
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
/**
* A filter condition for the room list which reveals rooms of a particular
@ -45,7 +46,24 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
}
public isVisible(room: Room): boolean {
// TODO: Improve this filter to include aliases and such
return room.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
const lcFilter = this.search.toLowerCase();
if (this.search[0] === '#') {
// Try and find rooms by alias
if (room.getCanonicalAlias() && room.getCanonicalAlias().toLowerCase().startsWith(lcFilter)) {
return true;
}
if (room.getAltAliases().some(a => a.toLowerCase().startsWith(lcFilter))) {
return true;
}
}
if (!room.name) return false; // should realisitically not happen: the js-sdk always calculates a name
// Note: we have to match the filter with the removeHiddenChars() room name because the
// function strips spaces and other characters (M becomes RN for example, in lowercase).
// We also doubly convert to lowercase to work around oddities of the library.
const noSecretsFilter = removeHiddenChars(lcFilter).toLowerCase();
const noSecretsName = removeHiddenChars(room.name.toLowerCase()).toLowerCase();
return noSecretsName.includes(noSecretsFilter);
}
}

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 {

26
src/utils/presence.ts Normal file
View file

@ -0,0 +1,26 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClientPeg } from "../MatrixClientPeg";
import SdkConfig from "../SdkConfig";
export function isPresenceEnabled() {
const hsUrl = MatrixClientPeg.get().baseUrl;
const urls = SdkConfig.get()['enable_presence_by_hs_url'];
if (!urls) return true;
if (urls[hsUrl] || urls[hsUrl] === undefined) return true;
return false;
}

View file

@ -61,4 +61,16 @@ describe('editor/serialize', function() {
const html = htmlSerializeIfNeeded(model, {});
expect(html).toBe("<a href=\"https://matrix.to/#/@user:server\">Displayname]</a>");
});
it('escaped markdown should not retain backslashes', function() {
const pc = createPartCreator();
const model = new EditorModel([pc.plain('\\*hello\\* world')]);
const html = htmlSerializeIfNeeded(model, {});
expect(html).toBe('*hello* world');
});
it('escaped markdown should convert HTML entities', function() {
const pc = createPartCreator();
const model = new EditorModel([pc.plain('\\*hello\\* world < hey world!')]);
const html = htmlSerializeIfNeeded(model, {});
expect(html).toBe('*hello* world &lt; hey world!');
});
});

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": [

Some files were not shown because too many files have changed in this diff Show more