Merge remote-tracking branch 'origin/develop' into rav/no_preserve_hs_url
This commit is contained in:
commit
010a31dbd1
23 changed files with 672 additions and 162 deletions
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -1,3 +1,57 @@
|
||||||
|
Changes in [0.14.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7) (2018-12-10)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.2...v0.14.7)
|
||||||
|
|
||||||
|
* No changes since rc.2
|
||||||
|
|
||||||
|
Changes in [0.14.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.2) (2018-12-06)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.1...v0.14.7-rc.2)
|
||||||
|
|
||||||
|
* Ship the babelrc file to npm
|
||||||
|
[\#2332](https://github.com/matrix-org/matrix-react-sdk/pull/2332)
|
||||||
|
|
||||||
|
Changes in [0.14.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.1) (2018-12-06)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.6...v0.14.7-rc.1)
|
||||||
|
|
||||||
|
* Suppress CORS errors in the 'failed to join room' dialog
|
||||||
|
[\#2306](https://github.com/matrix-org/matrix-react-sdk/pull/2306)
|
||||||
|
* Check if users exist before inviting them and communicate errors
|
||||||
|
[\#2317](https://github.com/matrix-org/matrix-react-sdk/pull/2317)
|
||||||
|
* Update from Weblate.
|
||||||
|
[\#2328](https://github.com/matrix-org/matrix-react-sdk/pull/2328)
|
||||||
|
* Allow group summary to load when /users fails
|
||||||
|
[\#2326](https://github.com/matrix-org/matrix-react-sdk/pull/2326)
|
||||||
|
* Show correct text if passphrase is skipped
|
||||||
|
[\#2324](https://github.com/matrix-org/matrix-react-sdk/pull/2324)
|
||||||
|
* Add password strength meter to backup creation UI
|
||||||
|
[\#2294](https://github.com/matrix-org/matrix-react-sdk/pull/2294)
|
||||||
|
* Check upload limits before trying to upload large files
|
||||||
|
[\#1876](https://github.com/matrix-org/matrix-react-sdk/pull/1876)
|
||||||
|
* Support .well-known discovery
|
||||||
|
[\#2227](https://github.com/matrix-org/matrix-react-sdk/pull/2227)
|
||||||
|
* Make create key backup dialog async
|
||||||
|
[\#2291](https://github.com/matrix-org/matrix-react-sdk/pull/2291)
|
||||||
|
* Forgot to enable continue button on download
|
||||||
|
[\#2288](https://github.com/matrix-org/matrix-react-sdk/pull/2288)
|
||||||
|
* Online incremental megolm backups (v2)
|
||||||
|
[\#2169](https://github.com/matrix-org/matrix-react-sdk/pull/2169)
|
||||||
|
* Add recovery key download button
|
||||||
|
[\#2284](https://github.com/matrix-org/matrix-react-sdk/pull/2284)
|
||||||
|
* Passphrase Support for e2e backups
|
||||||
|
[\#2283](https://github.com/matrix-org/matrix-react-sdk/pull/2283)
|
||||||
|
* Update async dialog interface to use promises
|
||||||
|
[\#2286](https://github.com/matrix-org/matrix-react-sdk/pull/2286)
|
||||||
|
* Support for m.login.sso
|
||||||
|
[\#2279](https://github.com/matrix-org/matrix-react-sdk/pull/2279)
|
||||||
|
* Added badge to non-autoplay GIFs
|
||||||
|
[\#2235](https://github.com/matrix-org/matrix-react-sdk/pull/2235)
|
||||||
|
* Improve terms auth flow
|
||||||
|
[\#2277](https://github.com/matrix-org/matrix-react-sdk/pull/2277)
|
||||||
|
* Handle crypto db version upgrade
|
||||||
|
[\#2282](https://github.com/matrix-org/matrix-react-sdk/pull/2282)
|
||||||
|
|
||||||
Changes in [0.14.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.6) (2018-11-22)
|
Changes in [0.14.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.6) (2018-11-22)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5...v0.14.6)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5...v0.14.6)
|
||||||
|
|
58
README.md
58
README.md
|
@ -127,61 +127,3 @@ Github Issues
|
||||||
|
|
||||||
All issues should be filed under https://github.com/vector-im/riot-web/issues
|
All issues should be filed under https://github.com/vector-im/riot-web/issues
|
||||||
for now.
|
for now.
|
||||||
|
|
||||||
OUTDATED: To Create Your Own Skin
|
|
||||||
=================================
|
|
||||||
|
|
||||||
**This is ALL LIES currently, and needs to be updated**
|
|
||||||
|
|
||||||
Skins are modules are exported from such a package in the `lib` directory.
|
|
||||||
`lib/skins` contains one directory per-skin, named after the skin, and the
|
|
||||||
`modules` directory contains modules as their javascript files.
|
|
||||||
|
|
||||||
A basic skin is provided in the matrix-react-skin package. This also contains
|
|
||||||
a minimal application that instantiates the basic skin making a working matrix
|
|
||||||
client.
|
|
||||||
|
|
||||||
You can use matrix-react-sdk directly, but to do this you would have to provide
|
|
||||||
'views' for each UI component. To get started quickly, use matrix-react-skin.
|
|
||||||
|
|
||||||
To actually change the look of a skin, you can create a base skin (which
|
|
||||||
does not use views from any other skin) or you can make a derived skin.
|
|
||||||
Note that derived skins are currently experimental: for example, the CSS
|
|
||||||
from the skins it is based on will not be automatically included.
|
|
||||||
|
|
||||||
To make a skin, create React classes for any custom components you wish to add
|
|
||||||
in a skin within `src/skins/<skin name>`. These can be based off the files in
|
|
||||||
`views` in the `matrix-react-skin` package, modifying the require() statement
|
|
||||||
appropriately.
|
|
||||||
|
|
||||||
If you make a derived skin, you only need copy the files you wish to customise.
|
|
||||||
|
|
||||||
Once you've made all your view files, you need to make a `skinfo.json`. This
|
|
||||||
contains all the metadata for a skin. This is a JSON file with, currently, a
|
|
||||||
single key, 'baseSkin'. Set this to the empty string if your skin is a base skin,
|
|
||||||
or for a derived skin, set it to the path of your base skin's skinfo.json file, as
|
|
||||||
you would use in a require call.
|
|
||||||
|
|
||||||
Now you have the basis of a skin, you need to generate a skindex.json file. The
|
|
||||||
`reskindex.js` tool in matrix-react-sdk does this for you. It is suggested that
|
|
||||||
you add an npm script to run this, as in matrix-react-skin.
|
|
||||||
|
|
||||||
For more specific detail on any of these steps, look at matrix-react-skin.
|
|
||||||
|
|
||||||
Alternative instructions:
|
|
||||||
|
|
||||||
* Create a new NPM project. Be sure to directly depend on react, (otherwise
|
|
||||||
you can end up with two copies of react).
|
|
||||||
* Create an index.js file that sets up react. Add require statements for
|
|
||||||
React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the
|
|
||||||
SDK and call Render. This can be a skin provided by a separate package or
|
|
||||||
a skin in the same package.
|
|
||||||
* Add a way to build your project: we suggest copying the scripts block
|
|
||||||
from matrix-react-skin (which uses babel and webpack). You could use
|
|
||||||
different tools but remember that at least the skins and modules of
|
|
||||||
your project should end up in plain (ie. non ES6, non JSX) javascript in
|
|
||||||
the lib directory at the end of the build process, as well as any
|
|
||||||
packaging that you might do.
|
|
||||||
* Create an index.html file pulling in your compiled javascript and the
|
|
||||||
CSS bundle from the skin you use. For now, you'll also need to manually
|
|
||||||
import CSS from any skins that your skin inherts from.
|
|
||||||
|
|
|
@ -165,7 +165,6 @@ ECMAScript
|
||||||
|
|
||||||
React
|
React
|
||||||
-----
|
-----
|
||||||
- Use React.createClass rather than ES6 classes for components, as the boilerplate is way too heavy on ES6 currently. ES7 might improve it.
|
|
||||||
- Pull out functions in props to the class, generally as specific event handlers:
|
- Pull out functions in props to the class, generally as specific event handlers:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
|
@ -174,11 +173,38 @@ React
|
||||||
<Foo onClick={this.doStuff}> // Better
|
<Foo onClick={this.doStuff}> // Better
|
||||||
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
|
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
|
||||||
```
|
```
|
||||||
|
|
||||||
Not doing so is acceptable in a single case; in function-refs:
|
Not doing so is acceptable in a single case: in function-refs:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<Foo ref={(self) => this.component = self}>
|
<Foo ref={(self) => this.component = self}>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass`
|
||||||
|
- You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor):
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Widget extends React.Component
|
||||||
|
onFooClick = () => {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- To define `propTypes`, use a static property:
|
||||||
|
```js
|
||||||
|
class Widget extends React.Component
|
||||||
|
static propTypes = {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- If you need to specify initial component state, [assign it](https://reactjs.org/docs/react-component.html#constructor) to `this.state` in the constructor:
|
||||||
|
```js
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// Don't call this.setState() here!
|
||||||
|
this.state = { counter: 0 };
|
||||||
|
}
|
||||||
|
```
|
||||||
- Think about whether your component really needs state: are you duplicating
|
- Think about whether your component really needs state: are you duplicating
|
||||||
information in component state that could be derived from the model?
|
information in component state that could be derived from the model?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.14.6",
|
"version": "0.14.7",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
".babelrc",
|
||||||
".eslintrc.js",
|
".eslintrc.js",
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"CONTRIBUTING.rst",
|
"CONTRIBUTING.rst",
|
||||||
|
@ -72,11 +73,12 @@
|
||||||
"gfm.css": "^1.1.1",
|
"gfm.css": "^1.1.1",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"highlight.js": "^9.13.0",
|
"highlight.js": "^9.13.0",
|
||||||
|
"is-ip": "^2.0.0",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.6",
|
"linkifyjs": "^2.1.6",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"lolex": "2.3.2",
|
"lolex": "2.3.2",
|
||||||
"matrix-js-sdk": "0.14.1",
|
"matrix-js-sdk": "0.14.2",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.5",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
|
|
|
@ -101,6 +101,7 @@
|
||||||
@import "./views/rooms/_RoomHeader.scss";
|
@import "./views/rooms/_RoomHeader.scss";
|
||||||
@import "./views/rooms/_RoomList.scss";
|
@import "./views/rooms/_RoomList.scss";
|
||||||
@import "./views/rooms/_RoomPreviewBar.scss";
|
@import "./views/rooms/_RoomPreviewBar.scss";
|
||||||
|
@import "./views/rooms/_RoomRecoveryReminder.scss";
|
||||||
@import "./views/rooms/_RoomSettings.scss";
|
@import "./views/rooms/_RoomSettings.scss";
|
||||||
@import "./views/rooms/_RoomTile.scss";
|
@import "./views/rooms/_RoomTile.scss";
|
||||||
@import "./views/rooms/_RoomTooltip.scss";
|
@import "./views/rooms/_RoomTooltip.scss";
|
||||||
|
|
43
res/css/views/rooms/_RoomRecoveryReminder.scss
Normal file
43
res/css/views/rooms/_RoomRecoveryReminder.scss
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_RoomRecoveryReminder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
background-color: $room-warning-bg-color;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid $primary-hairline-color;
|
||||||
|
border-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomRecoveryReminder_header {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomRecoveryReminder_body {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomRecoveryReminder_button {
|
||||||
|
@mixin mx_DialogButton;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary {
|
||||||
|
@mixin mx_DialogButton_secondary;
|
||||||
|
}
|
|
@ -100,6 +100,8 @@ $voip-accept-color: #80f480;
|
||||||
$rte-bg-color: #353535;
|
$rte-bg-color: #353535;
|
||||||
$rte-code-bg-color: #000;
|
$rte-code-bg-color: #000;
|
||||||
|
|
||||||
|
$room-warning-bg-color: #2d2d2d;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$roomtile-name-color: rgba(186, 186, 186, 0.8);
|
$roomtile-name-color: rgba(186, 186, 186, 0.8);
|
||||||
|
@ -169,6 +171,14 @@ $progressbar-color: #000;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@define-mixin mx_DialogButton_secondary {
|
||||||
|
// flip colours for the secondary ones
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid $accent-color ! important;
|
||||||
|
color: $accent-color;
|
||||||
|
background-color: $accent-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
// Nasty hacks to apply a filter to arbitrary monochrome artwork to make it
|
// Nasty hacks to apply a filter to arbitrary monochrome artwork to make it
|
||||||
// better match the theme. Typically applied to dark grey 'off' buttons or
|
// better match the theme. Typically applied to dark grey 'off' buttons or
|
||||||
// light grey 'on' buttons.
|
// light grey 'on' buttons.
|
||||||
|
|
|
@ -155,6 +155,8 @@ $imagebody-giflabel-border: rgba(0, 0, 0, 0.2);
|
||||||
// unused?
|
// unused?
|
||||||
$progressbar-color: #000;
|
$progressbar-color: #000;
|
||||||
|
|
||||||
|
$room-warning-bg-color: #fff8e3;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
@ -187,3 +189,11 @@ $progressbar-color: #000;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding: 0px 1.5em 0px 1.5em;
|
padding: 0px 1.5em 0px 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@define-mixin mx_DialogButton_secondary {
|
||||||
|
// flip colours for the secondary ones
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid $accent-color ! important;
|
||||||
|
color: $accent-color;
|
||||||
|
background-color: $accent-fg-color;
|
||||||
|
}
|
||||||
|
|
|
@ -222,10 +222,21 @@ const translatables = new Set();
|
||||||
|
|
||||||
const walkOpts = {
|
const walkOpts = {
|
||||||
listeners: {
|
listeners: {
|
||||||
|
names: function(root, nodeNamesArray) {
|
||||||
|
// Sort the names case insensitively and alphabetically to
|
||||||
|
// maintain some sense of order between the different strings.
|
||||||
|
nodeNamesArray.sort((a, b) => {
|
||||||
|
a = a.toLowerCase();
|
||||||
|
b = b.toLowerCase();
|
||||||
|
if (a > b) return 1;
|
||||||
|
if (a < b) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
file: function(root, fileStats, next) {
|
file: function(root, fileStats, next) {
|
||||||
const fullPath = path.join(root, fileStats.name);
|
const fullPath = path.join(root, fileStats.name);
|
||||||
|
|
||||||
let ltrs;
|
let trs;
|
||||||
if (fileStats.name.endsWith('.js')) {
|
if (fileStats.name.endsWith('.js')) {
|
||||||
trs = getTranslationsJs(fullPath);
|
trs = getTranslationsJs(fullPath);
|
||||||
} else if (fileStats.name.endsWith('.html')) {
|
} else if (fileStats.name.endsWith('.html')) {
|
||||||
|
@ -235,7 +246,8 @@ const walkOpts = {
|
||||||
}
|
}
|
||||||
console.log(`${fullPath} (${trs.size} strings)`);
|
console.log(`${fullPath} (${trs.size} strings)`);
|
||||||
for (const tr of trs.values()) {
|
for (const tr of trs.values()) {
|
||||||
translatables.add(tr);
|
// Convert DOS line endings to unix
|
||||||
|
translatables.add(tr.replace(/\r\n/g, "\n"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -251,7 +251,7 @@ export default React.createClass({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"If you don't want encrypted message history to be availble on other devices, "+
|
"If you don't want encrypted message history to be available on other devices, "+
|
||||||
"<button>opt out</button>.",
|
"<button>opt out</button>.",
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import sdk from "../../../../index";
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
|
||||||
|
export default class IgnoreRecoveryReminderDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
onDontAskAgain: PropTypes.func.isRequired,
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
onSetup: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
onDontAskAgainClick = () => {
|
||||||
|
this.props.onFinished();
|
||||||
|
this.props.onDontAskAgain();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSetupClick = () => {
|
||||||
|
this.props.onFinished();
|
||||||
|
this.props.onSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||||
|
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className="mx_IgnoreRecoveryReminderDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={_t("Are you sure?")}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>{_t(
|
||||||
|
"Without setting up Secure Message Recovery, " +
|
||||||
|
"you'll lose your secure message history when you " +
|
||||||
|
"log out.",
|
||||||
|
)}</p>
|
||||||
|
<p>{_t(
|
||||||
|
"If you don't want to set this up now, you can later " +
|
||||||
|
"in Settings.",
|
||||||
|
)}</p>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("Set up")}
|
||||||
|
onPrimaryButtonClick={this.onSetupClick}
|
||||||
|
cancelButton={_t("Don't ask again")}
|
||||||
|
onCancel={this.onDontAskAgainClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -327,17 +327,10 @@ const RoomSubList = React.createClass({
|
||||||
|
|
||||||
let incomingCall;
|
let incomingCall;
|
||||||
if (this.props.incomingCall) {
|
if (this.props.incomingCall) {
|
||||||
const self = this;
|
// We can assume that if we have an incoming call then it is for this list
|
||||||
// Check if the incoming call is for this section
|
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||||
const incomingCallRoom = this.props.list.filter(function(room) {
|
incomingCall =
|
||||||
return self.props.incomingCall.roomId === room.roomId;
|
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||||
});
|
|
||||||
|
|
||||||
if (incomingCallRoom.length === 1) {
|
|
||||||
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
|
||||||
incomingCall =
|
|
||||||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||||
|
|
|
@ -607,6 +607,20 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async onRoomRecoveryReminderFinished(backupCreated) {
|
||||||
|
// If the user cancelled the key backup dialog, it suggests they don't
|
||||||
|
// want to be reminded anymore.
|
||||||
|
if (!backupCreated) {
|
||||||
|
await SettingsStore.setValue(
|
||||||
|
"showRoomRecoveryReminder",
|
||||||
|
null,
|
||||||
|
SettingLevel.ACCOUNT,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
canResetTimeline: function() {
|
canResetTimeline: function() {
|
||||||
if (!this.refs.messagePanel) {
|
if (!this.refs.messagePanel) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -1521,6 +1535,7 @@ module.exports = React.createClass({
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||||
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
|
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
|
||||||
|
const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
|
||||||
|
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
if (this.state.roomLoading || this.state.peekLoading) {
|
if (this.state.roomLoading || this.state.peekLoading) {
|
||||||
|
@ -1655,6 +1670,13 @@ module.exports = React.createClass({
|
||||||
this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId)
|
this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showRoomRecoveryReminder = (
|
||||||
|
SettingsStore.isFeatureEnabled("feature_keybackup") &&
|
||||||
|
SettingsStore.getValue("showRoomRecoveryReminder") &&
|
||||||
|
MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) &&
|
||||||
|
!MatrixClientPeg.get().getKeyBackupEnabled()
|
||||||
|
);
|
||||||
|
|
||||||
let aux = null;
|
let aux = null;
|
||||||
let hideCancel = false;
|
let hideCancel = false;
|
||||||
if (this.state.editingRoomSettings) {
|
if (this.state.editingRoomSettings) {
|
||||||
|
@ -1669,6 +1691,9 @@ module.exports = React.createClass({
|
||||||
} else if (showRoomUpgradeBar) {
|
} else if (showRoomUpgradeBar) {
|
||||||
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
||||||
hideCancel = true;
|
hideCancel = true;
|
||||||
|
} else if (showRoomRecoveryReminder) {
|
||||||
|
aux = <RoomRecoveryReminder onFinished={this.onRoomRecoveryReminderFinished} />;
|
||||||
|
hideCancel = true;
|
||||||
} else if (this.state.showingPinned) {
|
} else if (this.state.showingPinned) {
|
||||||
hideCancel = true; // has own cancel
|
hideCancel = true; // has own cancel
|
||||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
||||||
|
|
|
@ -64,6 +64,7 @@ const SIMPLE_SETTINGS = [
|
||||||
{ id: "urlPreviewsEnabled" },
|
{ id: "urlPreviewsEnabled" },
|
||||||
{ id: "autoplayGifsAndVideos" },
|
{ id: "autoplayGifsAndVideos" },
|
||||||
{ id: "alwaysShowEncryptionIcons" },
|
{ id: "alwaysShowEncryptionIcons" },
|
||||||
|
{ id: "showRoomRecoveryReminder" },
|
||||||
{ id: "hideReadReceipts" },
|
{ id: "hideReadReceipts" },
|
||||||
{ id: "dontSendTypingNotifications" },
|
{ id: "dontSendTypingNotifications" },
|
||||||
{ id: "alwaysShowTimestamps" },
|
{ id: "alwaysShowTimestamps" },
|
||||||
|
|
|
@ -35,19 +35,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||||
|
|
||||||
const deactivationPreferences =
|
|
||||||
MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences');
|
|
||||||
|
|
||||||
const shouldErase = (
|
|
||||||
deactivationPreferences &&
|
|
||||||
deactivationPreferences.getContent() &&
|
|
||||||
deactivationPreferences.getContent().shouldErase
|
|
||||||
) || false;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
confirmButtonEnabled: false,
|
confirmButtonEnabled: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
shouldErase,
|
shouldErase: false,
|
||||||
errStr: null,
|
errStr: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -67,36 +58,6 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
async _onOk() {
|
async _onOk() {
|
||||||
this.setState({busy: true});
|
this.setState({busy: true});
|
||||||
|
|
||||||
// Before we deactivate the account insert an event into
|
|
||||||
// the user's account data indicating that they wish to be
|
|
||||||
// erased from the homeserver.
|
|
||||||
//
|
|
||||||
// We do this because the API for erasing after deactivation
|
|
||||||
// might not be supported by the connected homeserver. Leaving
|
|
||||||
// an indication in account data is only best-effort, and
|
|
||||||
// in the worse case, the HS maintainer would have to run a
|
|
||||||
// script to erase deactivated accounts that have shouldErase
|
|
||||||
// set to true in im.riot.account_deactivation_preferences.
|
|
||||||
//
|
|
||||||
// Note: The preferences are scoped to Riot, hence the
|
|
||||||
// "im.riot..." event type.
|
|
||||||
//
|
|
||||||
// Note: This may have already been set on previous attempts
|
|
||||||
// where, for example, the user entered the wrong password.
|
|
||||||
// This is fine because the UI always indicates the preference
|
|
||||||
// prior to us calling `deactivateAccount`.
|
|
||||||
try {
|
|
||||||
await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', {
|
|
||||||
shouldErase: this.state.shouldErase,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({
|
|
||||||
busy: false,
|
|
||||||
errStr: _t('Failed to indicate account erasure'),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This assumes that the HS requires password UI auth
|
// This assumes that the HS requires password UI auth
|
||||||
// for this endpoint. In reality it could be any UI auth.
|
// for this endpoint. In reality it could be any UI auth.
|
||||||
|
|
|
@ -71,6 +71,7 @@ module.exports = React.createClass({
|
||||||
isLoadingLeftRooms: false,
|
isLoadingLeftRooms: false,
|
||||||
totalRoomCount: null,
|
totalRoomCount: null,
|
||||||
lists: {},
|
lists: {},
|
||||||
|
incomingCallTag: null,
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
selectedTags: [],
|
selectedTags: [],
|
||||||
};
|
};
|
||||||
|
@ -155,11 +156,13 @@ module.exports = React.createClass({
|
||||||
if (call && call.call_state === 'ringing') {
|
if (call && call.call_state === 'ringing') {
|
||||||
this.setState({
|
this.setState({
|
||||||
incomingCall: call,
|
incomingCall: call,
|
||||||
|
incomingCallTag: this.getTagNameForRoomId(payload.room_id),
|
||||||
});
|
});
|
||||||
this._repositionIncomingCallBox(undefined, true);
|
this._repositionIncomingCallBox(undefined, true);
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
|
incomingCallTag: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -328,6 +331,26 @@ module.exports = React.createClass({
|
||||||
// this._lastRefreshRoomListTs = Date.now();
|
// this._lastRefreshRoomListTs = Date.now();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTagNameForRoomId: function(roomId) {
|
||||||
|
const lists = RoomListStore.getRoomLists();
|
||||||
|
for (const tagName of Object.keys(lists)) {
|
||||||
|
for (const room of lists[tagName]) {
|
||||||
|
// Should be impossible, but guard anyways.
|
||||||
|
if (!room) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, myUserId, this.props.ConferenceHandler)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room.roomId === roomId) return tagName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
getRoomLists: function() {
|
getRoomLists: function() {
|
||||||
const lists = RoomListStore.getRoomLists();
|
const lists = RoomListStore.getRoomLists();
|
||||||
|
|
||||||
|
@ -621,6 +644,12 @@ module.exports = React.createClass({
|
||||||
// so checking on every render is the sanest thing at this time.
|
// so checking on every render is the sanest thing at this time.
|
||||||
const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty');
|
const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty');
|
||||||
|
|
||||||
|
const incomingCallIfTaggedAs = (tagName) => {
|
||||||
|
if (!this.state.incomingCall) return null;
|
||||||
|
if (this.state.incomingCallTag !== tagName) return null;
|
||||||
|
return this.state.incomingCall;
|
||||||
|
};
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
return (
|
return (
|
||||||
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
|
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
|
||||||
|
@ -644,7 +673,7 @@ module.exports = React.createClass({
|
||||||
editable={false}
|
editable={false}
|
||||||
order="recent"
|
order="recent"
|
||||||
isInvite={true}
|
isInvite={true}
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={incomingCallIfTaggedAs('im.vector.fake.invite')}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
|
@ -658,7 +687,7 @@ module.exports = React.createClass({
|
||||||
emptyContent={this._getEmptyContent('m.favourite')}
|
emptyContent={this._getEmptyContent('m.favourite')}
|
||||||
editable={true}
|
editable={true}
|
||||||
order="manual"
|
order="manual"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={incomingCallIfTaggedAs('m.favourite')}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
|
@ -672,7 +701,7 @@ module.exports = React.createClass({
|
||||||
headerItems={this._getHeaderItems('im.vector.fake.direct')}
|
headerItems={this._getHeaderItems('im.vector.fake.direct')}
|
||||||
editable={true}
|
editable={true}
|
||||||
order="recent"
|
order="recent"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={incomingCallIfTaggedAs('im.vector.fake.direct')}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
alwaysShowHeader={true}
|
alwaysShowHeader={true}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
|
@ -686,7 +715,7 @@ module.exports = React.createClass({
|
||||||
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
||||||
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
||||||
order="recent"
|
order="recent"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={incomingCallIfTaggedAs('im.vector.fake.recent')}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
|
@ -702,7 +731,7 @@ module.exports = React.createClass({
|
||||||
emptyContent={this._getEmptyContent(tagName)}
|
emptyContent={this._getEmptyContent(tagName)}
|
||||||
editable={true}
|
editable={true}
|
||||||
order="manual"
|
order="manual"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={incomingCallIfTaggedAs(tagName)}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
|
@ -717,7 +746,7 @@ module.exports = React.createClass({
|
||||||
emptyContent={this._getEmptyContent('m.lowpriority')}
|
emptyContent={this._getEmptyContent('m.lowpriority')}
|
||||||
editable={true}
|
editable={true}
|
||||||
order="recent"
|
order="recent"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={incomingCallIfTaggedAs('m.lowpriority')}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
|
@ -740,7 +769,7 @@ module.exports = React.createClass({
|
||||||
startAsHidden={true}
|
startAsHidden={true}
|
||||||
showSpinner={self.state.isLoadingLeftRooms}
|
showSpinner={self.state.isLoadingLeftRooms}
|
||||||
onHeaderClick={self.onArchivedHeaderClick}
|
onHeaderClick={self.onArchivedHeaderClick}
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={incomingCallIfTaggedAs('im.vector.fake.archived')}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onShowMoreRooms={self.onShowMoreRooms}
|
onShowMoreRooms={self.onShowMoreRooms}
|
||||||
showEmpty={showEmpty} />
|
showEmpty={showEmpty} />
|
||||||
|
@ -750,7 +779,7 @@ module.exports = React.createClass({
|
||||||
tagName="m.lowpriority"
|
tagName="m.lowpriority"
|
||||||
editable={false}
|
editable={false}
|
||||||
order="recent"
|
order="recent"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={incomingCallIfTaggedAs('m.server_notice')}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
|
|
85
src/components/views/rooms/RoomRecoveryReminder.js
Normal file
85
src/components/views/rooms/RoomRecoveryReminder.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import sdk from "../../../index";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
|
||||||
|
export default class RoomRecoveryReminder extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
showKeyBackupDialog = () => {
|
||||||
|
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||||
|
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||||
|
{
|
||||||
|
onFinished: this.props.onFinished,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDontAskAgainClick = () => {
|
||||||
|
// When you choose "Don't ask again" from the room reminder, we show a
|
||||||
|
// dialog to confirm the choice.
|
||||||
|
Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder",
|
||||||
|
import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"),
|
||||||
|
{
|
||||||
|
onDontAskAgain: () => {
|
||||||
|
// Report false to the caller, who should prevent the
|
||||||
|
// reminder from appearing in the future.
|
||||||
|
this.props.onFinished(false);
|
||||||
|
},
|
||||||
|
onSetup: () => {
|
||||||
|
this.showKeyBackupDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSetupClick = () => {
|
||||||
|
this.showKeyBackupDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomRecoveryReminder">
|
||||||
|
<div className="mx_RoomRecoveryReminder_header">{_t(
|
||||||
|
"Secure Message Recovery",
|
||||||
|
)}</div>
|
||||||
|
<div className="mx_RoomRecoveryReminder_body">{_t(
|
||||||
|
"If you log out or use another device, you'll lose your " +
|
||||||
|
"secure message history. To prevent this, set up Secure " +
|
||||||
|
"Message Recovery.",
|
||||||
|
)}</div>
|
||||||
|
<div className="mx_RoomRecoveryReminder_buttons">
|
||||||
|
<AccessibleButton className="mx_RoomRecoveryReminder_button mx_RoomRecoveryReminder_secondary"
|
||||||
|
onClick={this.onDontAskAgainClick}>
|
||||||
|
{ _t("Don't ask again") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton className="mx_RoomRecoveryReminder_button"
|
||||||
|
onClick={this.onSetupClick}>
|
||||||
|
{ _t("Set up") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,6 +43,10 @@
|
||||||
"The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload",
|
"The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload",
|
||||||
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads",
|
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads",
|
||||||
"Upload Failed": "Upload Failed",
|
"Upload Failed": "Upload Failed",
|
||||||
|
"Failure to create room": "Failure to create room",
|
||||||
|
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
|
||||||
|
"Send anyway": "Send anyway",
|
||||||
|
"Send": "Send",
|
||||||
"Sun": "Sun",
|
"Sun": "Sun",
|
||||||
"Mon": "Mon",
|
"Mon": "Mon",
|
||||||
"Tue": "Tue",
|
"Tue": "Tue",
|
||||||
|
@ -82,6 +86,7 @@
|
||||||
"Failed to invite users to community": "Failed to invite users to community",
|
"Failed to invite users to community": "Failed to invite users to community",
|
||||||
"Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s",
|
"Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s",
|
||||||
"Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:",
|
"Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:",
|
||||||
|
"Unnamed Room": "Unnamed Room",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
|
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
|
||||||
"Dismiss": "Dismiss",
|
"Dismiss": "Dismiss",
|
||||||
|
@ -210,11 +215,6 @@
|
||||||
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
|
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
|
||||||
"%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing",
|
"%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing",
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
|
||||||
"Failure to create room": "Failure to create room",
|
|
||||||
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
|
|
||||||
"Send anyway": "Send anyway",
|
|
||||||
"Send": "Send",
|
|
||||||
"Unnamed Room": "Unnamed Room",
|
|
||||||
"This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.",
|
"This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.",
|
||||||
"This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.",
|
"This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.",
|
||||||
"Please <a>contact your service administrator</a> to continue using the service.": "Please <a>contact your service administrator</a> to continue using the service.",
|
"Please <a>contact your service administrator</a> to continue using the service.": "Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
|
@ -222,7 +222,9 @@
|
||||||
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
|
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
|
||||||
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
||||||
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
||||||
"There was an error joining the room": "There was an error joining the room",
|
"You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.",
|
||||||
|
"User %(user_id)s does not exist": "User %(user_id)s does not exist",
|
||||||
|
"Unknown server error": "Unknown server error",
|
||||||
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
|
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
|
||||||
"No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters",
|
"No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters",
|
||||||
"Use a longer keyboard pattern with more turns": "Use a longer keyboard pattern with more turns",
|
"Use a longer keyboard pattern with more turns": "Use a longer keyboard pattern with more turns",
|
||||||
|
@ -248,9 +250,7 @@
|
||||||
"A word by itself is easy to guess": "A word by itself is easy to guess",
|
"A word by itself is easy to guess": "A word by itself is easy to guess",
|
||||||
"Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess",
|
"Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess",
|
||||||
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
|
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
|
||||||
"You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.",
|
"There was an error joining the room": "There was an error joining the room",
|
||||||
"User %(user_id)s does not exist": "User %(user_id)s does not exist",
|
|
||||||
"Unknown server error": "Unknown server error",
|
|
||||||
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
||||||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||||
"Failed to join room": "Failed to join room",
|
"Failed to join room": "Failed to join room",
|
||||||
|
@ -268,6 +268,7 @@
|
||||||
"Always show message timestamps": "Always show message timestamps",
|
"Always show message timestamps": "Always show message timestamps",
|
||||||
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
|
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
|
||||||
"Always show encryption icons": "Always show encryption icons",
|
"Always show encryption icons": "Always show encryption icons",
|
||||||
|
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Show a reminder to enable Secure Message Recovery in encrypted rooms",
|
||||||
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
|
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
|
||||||
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
|
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
|
||||||
"Disable big emoji in chat": "Disable big emoji in chat",
|
"Disable big emoji in chat": "Disable big emoji in chat",
|
||||||
|
@ -491,11 +492,11 @@
|
||||||
"At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.",
|
"At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.",
|
||||||
"Markdown is disabled": "Markdown is disabled",
|
"Markdown is disabled": "Markdown is disabled",
|
||||||
"Markdown is enabled": "Markdown is enabled",
|
"Markdown is enabled": "Markdown is enabled",
|
||||||
"Unpin Message": "Unpin Message",
|
|
||||||
"Jump to message": "Jump to message",
|
|
||||||
"No pinned messages.": "No pinned messages.",
|
"No pinned messages.": "No pinned messages.",
|
||||||
"Loading...": "Loading...",
|
"Loading...": "Loading...",
|
||||||
"Pinned Messages": "Pinned Messages",
|
"Pinned Messages": "Pinned Messages",
|
||||||
|
"Unpin Message": "Unpin Message",
|
||||||
|
"Jump to message": "Jump to message",
|
||||||
"%(duration)ss": "%(duration)ss",
|
"%(duration)ss": "%(duration)ss",
|
||||||
"%(duration)sm": "%(duration)sm",
|
"%(duration)sm": "%(duration)sm",
|
||||||
"%(duration)sh": "%(duration)sh",
|
"%(duration)sh": "%(duration)sh",
|
||||||
|
@ -562,6 +563,10 @@
|
||||||
"You are trying to access a room.": "You are trying to access a room.",
|
"You are trying to access a room.": "You are trying to access a room.",
|
||||||
"<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!",
|
"<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!",
|
||||||
"This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled",
|
"This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled",
|
||||||
|
"Secure Message Recovery": "Secure Message Recovery",
|
||||||
|
"If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.",
|
||||||
|
"Don't ask again": "Don't ask again",
|
||||||
|
"Set up": "Set up",
|
||||||
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
|
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
|
||||||
"To change the room's name, you must be a": "To change the room's name, you must be a",
|
"To change the room's name, you must be a": "To change the room's name, you must be a",
|
||||||
"To change the room's main address, you must be a": "To change the room's main address, you must be a",
|
"To change the room's main address, you must be a": "To change the room's main address, you must be a",
|
||||||
|
@ -734,6 +739,7 @@
|
||||||
"Remove this user from community?": "Remove this user from community?",
|
"Remove this user from community?": "Remove this user from community?",
|
||||||
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
||||||
"Failed to remove user from community": "Failed to remove user from community",
|
"Failed to remove user from community": "Failed to remove user from community",
|
||||||
|
"Failed to load group members": "Failed to load group members",
|
||||||
"Filter community members": "Filter community members",
|
"Filter community members": "Filter community members",
|
||||||
"Flair will appear if enabled in room settings": "Flair will appear if enabled in room settings",
|
"Flair will appear if enabled in room settings": "Flair will appear if enabled in room settings",
|
||||||
"Flair will not appear": "Flair will not appear",
|
"Flair will not appear": "Flair will not appear",
|
||||||
|
@ -1104,7 +1110,6 @@
|
||||||
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
||||||
"This Home server does not support communities": "This Home server does not support communities",
|
"This Home server does not support communities": "This Home server does not support communities",
|
||||||
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
|
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
|
||||||
"Failed to load group members": "Failed to load group members",
|
|
||||||
"Couldn't load home page": "Couldn't load home page",
|
"Couldn't load home page": "Couldn't load home page",
|
||||||
"You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.",
|
"You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.",
|
||||||
"If you would like to create a Matrix account you can <a>register</a> now.": "If you would like to create a Matrix account you can <a>register</a> now.",
|
"If you would like to create a Matrix account you can <a>register</a> now.": "If you would like to create a Matrix account you can <a>register</a> now.",
|
||||||
|
@ -1352,7 +1357,7 @@
|
||||||
"Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.",
|
"Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.",
|
||||||
"You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.",
|
"You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.",
|
||||||
"Enter a passphrase...": "Enter a passphrase...",
|
"Enter a passphrase...": "Enter a passphrase...",
|
||||||
"If you don't want encrypted message history to be availble on other devices, <button>opt out</button>.": "If you don't want encrypted message history to be availble on other devices, <button>opt out</button>.",
|
"If you don't want encrypted message history to be available on other devices, <button>opt out</button>.": "If you don't want encrypted message history to be available on other devices, <button>opt out</button>.",
|
||||||
"Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.": "Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.",
|
"Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.": "Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.",
|
||||||
"That matches!": "That matches!",
|
"That matches!": "That matches!",
|
||||||
"That doesn't match.": "That doesn't match.",
|
"That doesn't match.": "That doesn't match.",
|
||||||
|
@ -1384,6 +1389,8 @@
|
||||||
"Create Key Backup": "Create Key Backup",
|
"Create Key Backup": "Create Key Backup",
|
||||||
"Unable to create key backup": "Unable to create key backup",
|
"Unable to create key backup": "Unable to create key backup",
|
||||||
"Retry": "Retry",
|
"Retry": "Retry",
|
||||||
|
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
|
||||||
|
"If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.",
|
||||||
"Failed to set direct chat tag": "Failed to set direct chat tag",
|
"Failed to set direct chat tag": "Failed to set direct chat tag",
|
||||||
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
|
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
|
||||||
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
|
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
|
||||||
|
|
|
@ -15,6 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MatrixClientPeg from "./MatrixClientPeg";
|
import MatrixClientPeg from "./MatrixClientPeg";
|
||||||
|
import isIp from "is-ip";
|
||||||
|
import utils from 'matrix-js-sdk/lib/utils';
|
||||||
|
|
||||||
export const host = "matrix.to";
|
export const host = "matrix.to";
|
||||||
export const baseUrl = `https://${host}`;
|
export const baseUrl = `https://${host}`;
|
||||||
|
@ -90,7 +92,9 @@ export function pickServerCandidates(roomId) {
|
||||||
// Rationale for popular servers: It's hard to get rid of people when
|
// Rationale for popular servers: It's hard to get rid of people when
|
||||||
// they keep flocking in from a particular server. Sure, the server could
|
// they keep flocking in from a particular server. Sure, the server could
|
||||||
// be ACL'd in the future or for some reason be evicted from the room
|
// be ACL'd in the future or for some reason be evicted from the room
|
||||||
// however an event like that is unlikely the larger the room gets.
|
// however an event like that is unlikely the larger the room gets. If
|
||||||
|
// the server is ACL'd at the time of generating the link however, we
|
||||||
|
// shouldn't pick them. We also don't pick IP addresses.
|
||||||
|
|
||||||
// Note: we don't pick the server the room was created on because the
|
// Note: we don't pick the server the room was created on because the
|
||||||
// homeserver should already be using that server as a last ditch attempt
|
// homeserver should already be using that server as a last ditch attempt
|
||||||
|
@ -104,12 +108,29 @@ export function pickServerCandidates(roomId) {
|
||||||
// The receiving user can then manually append the known-good server to
|
// The receiving user can then manually append the known-good server to
|
||||||
// the list and magically have the link work.
|
// the list and magically have the link work.
|
||||||
|
|
||||||
|
const bannedHostsRegexps = [];
|
||||||
|
let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
|
||||||
|
if (room.currentState) {
|
||||||
|
const aclEvent = room.currentState.getStateEvents("m.room.server_acl", "");
|
||||||
|
if (aclEvent && aclEvent.getContent()) {
|
||||||
|
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
|
||||||
|
|
||||||
|
const denied = aclEvent.getContent().deny || [];
|
||||||
|
denied.forEach(h => bannedHostsRegexps.push(getRegex(h)));
|
||||||
|
|
||||||
|
const allowed = aclEvent.getContent().allow || [];
|
||||||
|
allowedHostsRegexps = []; // we don't want to use the default rule here
|
||||||
|
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const populationMap: {[server:string]:number} = {};
|
const populationMap: {[server:string]:number} = {};
|
||||||
const highestPlUser = {userId: null, powerLevel: 0, serverName: null};
|
const highestPlUser = {userId: null, powerLevel: 0, serverName: null};
|
||||||
|
|
||||||
for (const member of room.getJoinedMembers()) {
|
for (const member of room.getJoinedMembers()) {
|
||||||
const serverName = member.userId.split(":").splice(1).join(":");
|
const serverName = member.userId.split(":").splice(1).join(":");
|
||||||
if (member.powerLevel > highestPlUser.powerLevel) {
|
if (member.powerLevel > highestPlUser.powerLevel && !isHostnameIpAddress(serverName)
|
||||||
|
&& !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) {
|
||||||
highestPlUser.userId = member.userId;
|
highestPlUser.userId = member.userId;
|
||||||
highestPlUser.powerLevel = member.powerLevel;
|
highestPlUser.powerLevel = member.powerLevel;
|
||||||
highestPlUser.serverName = serverName;
|
highestPlUser.serverName = serverName;
|
||||||
|
@ -125,8 +146,9 @@ export function pickServerCandidates(roomId) {
|
||||||
const beforePopulation = candidates.length;
|
const beforePopulation = candidates.length;
|
||||||
const serversByPopulation = Object.keys(populationMap)
|
const serversByPopulation = Object.keys(populationMap)
|
||||||
.sort((a, b) => populationMap[b] - populationMap[a])
|
.sort((a, b) => populationMap[b] - populationMap[a])
|
||||||
.filter(a => !candidates.includes(a));
|
.filter(a => !candidates.includes(a) && !isHostnameIpAddress(a)
|
||||||
for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) {
|
&& !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps));
|
||||||
|
for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) {
|
||||||
const idx = i - beforePopulation;
|
const idx = i - beforePopulation;
|
||||||
if (idx >= serversByPopulation.length) break;
|
if (idx >= serversByPopulation.length) break;
|
||||||
candidates.push(serversByPopulation[idx]);
|
candidates.push(serversByPopulation[idx]);
|
||||||
|
@ -134,3 +156,34 @@ export function pickServerCandidates(roomId) {
|
||||||
|
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHostnameFromMatrixDomain(domain) {
|
||||||
|
if (!domain) return null;
|
||||||
|
|
||||||
|
// The hostname might have a port, so we convert it to a URL and
|
||||||
|
// split out the real hostname.
|
||||||
|
const parser = document.createElement('a');
|
||||||
|
parser.href = "https://" + domain;
|
||||||
|
return parser.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHostInRegex(hostname, regexps) {
|
||||||
|
hostname = getHostnameFromMatrixDomain(hostname);
|
||||||
|
if (!hostname) return true; // assumed
|
||||||
|
if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]);
|
||||||
|
|
||||||
|
return regexps.filter(h => h.test(hostname)).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHostnameIpAddress(hostname) {
|
||||||
|
hostname = getHostnameFromMatrixDomain(hostname);
|
||||||
|
if (!hostname) return false;
|
||||||
|
|
||||||
|
// is-ip doesn't want IPv6 addresses surrounded by brackets, so
|
||||||
|
// take them off.
|
||||||
|
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
||||||
|
hostname = hostname.substring(1, hostname.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isIp(hostname);
|
||||||
|
}
|
||||||
|
|
|
@ -151,6 +151,11 @@ export const SETTINGS = {
|
||||||
displayName: _td('Always show encryption icons'),
|
displayName: _td('Always show encryption icons'),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
"showRoomRecoveryReminder": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
displayName: _td('Show a reminder to enable Secure Message Recovery in encrypted rooms'),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
"enableSyntaxHighlightLanguageDetection": {
|
"enableSyntaxHighlightLanguageDetection": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Enable automatic language detection for syntax highlighting'),
|
displayName: _td('Enable automatic language detection for syntax highlighting'),
|
||||||
|
|
|
@ -38,18 +38,20 @@ function memberEventDiff(ev) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function shouldHideEvent(ev) {
|
export default function shouldHideEvent(ev) {
|
||||||
// Wrap getValue() for readability
|
// Wrap getValue() for readability. Calling the SettingsStore can be
|
||||||
|
// fairly resource heavy, so the checks below should avoid hitting it
|
||||||
|
// where possible.
|
||||||
const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId());
|
const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId());
|
||||||
|
|
||||||
// Hide redacted events
|
// Hide redacted events
|
||||||
if (isEnabled('hideRedactions') && ev.isRedacted()) return true;
|
if (ev.isRedacted() && isEnabled('hideRedactions')) return true;
|
||||||
|
|
||||||
const eventDiff = memberEventDiff(ev);
|
const eventDiff = memberEventDiff(ev);
|
||||||
|
|
||||||
if (eventDiff.isMemberEvent) {
|
if (eventDiff.isMemberEvent) {
|
||||||
if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true;
|
if ((eventDiff.isJoin || eventDiff.isPart) && isEnabled('hideJoinLeaves')) return true;
|
||||||
if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true;
|
if (eventDiff.isAvatarChange && isEnabled('hideAvatarChanges')) return true;
|
||||||
if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true;
|
if (eventDiff.isDisplaynameChange && isEnabled('hideDisplaynameChanges')) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -300,6 +300,10 @@ class RoomListStore extends Store {
|
||||||
const ts = this._tsOfNewestEvent(room);
|
const ts = this._tsOfNewestEvent(room);
|
||||||
this._updateCachedRoomState(roomId, "timestamp", ts);
|
this._updateCachedRoomState(roomId, "timestamp", ts);
|
||||||
return ts;
|
return ts;
|
||||||
|
} else if (type === "unread-muted") {
|
||||||
|
const unread = Unread.doesRoomHaveUnreadMessages(room);
|
||||||
|
this._updateCachedRoomState(roomId, "unread-muted", unread);
|
||||||
|
return unread;
|
||||||
} else if (type === "unread") {
|
} else if (type === "unread") {
|
||||||
const unread = room.getUnreadNotificationCount() > 0;
|
const unread = room.getUnreadNotificationCount() > 0;
|
||||||
this._updateCachedRoomState(roomId, "unread", unread);
|
this._updateCachedRoomState(roomId, "unread", unread);
|
||||||
|
@ -358,8 +362,21 @@ class RoomListStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pinUnread) {
|
if (pinUnread) {
|
||||||
const unreadA = this._getRoomState(roomA, "unread");
|
let unreadA = this._getRoomState(roomA, "unread");
|
||||||
const unreadB = this._getRoomState(roomB, "unread");
|
let unreadB = this._getRoomState(roomB, "unread");
|
||||||
|
if (unreadA && !unreadB) return -1;
|
||||||
|
if (!unreadA && unreadB) return 1;
|
||||||
|
|
||||||
|
// If they both have unread messages, sort by timestamp
|
||||||
|
// If nether have unread message (the fourth check not shown
|
||||||
|
// here), then just sort by timestamp anyways.
|
||||||
|
if (unreadA && unreadB) return timestampDiff;
|
||||||
|
|
||||||
|
// Unread can also mean "unread without badge", which is
|
||||||
|
// different from what the above checks for. We're also
|
||||||
|
// going to sort those here.
|
||||||
|
unreadA = this._getRoomState(roomA, "unread-muted");
|
||||||
|
unreadB = this._getRoomState(roomB, "unread-muted");
|
||||||
if (unreadA && !unreadB) return -1;
|
if (unreadA && !unreadB) return -1;
|
||||||
if (!unreadA && unreadB) return 1;
|
if (!unreadA && unreadB) return 1;
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,39 @@ describe('matrix-to', function() {
|
||||||
expect(pickedServers[2]).toBe("third");
|
expect(pickedServers[2]).toBe("third");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with IPv4 hostnames', function() {
|
it('should pick a maximum of 3 candidate servers', function() {
|
||||||
|
peg.get().getRoom = () => {
|
||||||
|
return {
|
||||||
|
getJoinedMembers: () => [
|
||||||
|
{
|
||||||
|
userId: "@alice:alpha",
|
||||||
|
powerLevel: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@alice:bravo",
|
||||||
|
powerLevel: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@alice:charlie",
|
||||||
|
powerLevel: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@alice:delta",
|
||||||
|
powerLevel: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@alice:echo",
|
||||||
|
powerLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
|
expect(pickedServers).toExist();
|
||||||
|
expect(pickedServers.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not consider IPv4 hosts', function() {
|
||||||
peg.get().getRoom = () => {
|
peg.get().getRoom = () => {
|
||||||
return {
|
return {
|
||||||
getJoinedMembers: () => [
|
getJoinedMembers: () => [
|
||||||
|
@ -163,11 +195,10 @@ describe('matrix-to', function() {
|
||||||
};
|
};
|
||||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
expect(pickedServers).toExist();
|
expect(pickedServers).toExist();
|
||||||
expect(pickedServers.length).toBe(1);
|
expect(pickedServers.length).toBe(0);
|
||||||
expect(pickedServers[0]).toBe("127.0.0.1");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with IPv6 hostnames', function() {
|
it('should not consider IPv6 hosts', function() {
|
||||||
peg.get().getRoom = () => {
|
peg.get().getRoom = () => {
|
||||||
return {
|
return {
|
||||||
getJoinedMembers: () => [
|
getJoinedMembers: () => [
|
||||||
|
@ -180,11 +211,10 @@ describe('matrix-to', function() {
|
||||||
};
|
};
|
||||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
expect(pickedServers).toExist();
|
expect(pickedServers).toExist();
|
||||||
expect(pickedServers.length).toBe(1);
|
expect(pickedServers.length).toBe(0);
|
||||||
expect(pickedServers[0]).toBe("[::1]");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with IPv4 hostnames with ports', function() {
|
it('should not consider IPv4 hostnames with ports', function() {
|
||||||
peg.get().getRoom = () => {
|
peg.get().getRoom = () => {
|
||||||
return {
|
return {
|
||||||
getJoinedMembers: () => [
|
getJoinedMembers: () => [
|
||||||
|
@ -197,11 +227,10 @@ describe('matrix-to', function() {
|
||||||
};
|
};
|
||||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
expect(pickedServers).toExist();
|
expect(pickedServers).toExist();
|
||||||
expect(pickedServers.length).toBe(1);
|
expect(pickedServers.length).toBe(0);
|
||||||
expect(pickedServers[0]).toBe("127.0.0.1:8448");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with IPv6 hostnames with ports', function() {
|
it('should not consider IPv6 hostnames with ports', function() {
|
||||||
peg.get().getRoom = () => {
|
peg.get().getRoom = () => {
|
||||||
return {
|
return {
|
||||||
getJoinedMembers: () => [
|
getJoinedMembers: () => [
|
||||||
|
@ -214,8 +243,7 @@ describe('matrix-to', function() {
|
||||||
};
|
};
|
||||||
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
expect(pickedServers).toExist();
|
expect(pickedServers).toExist();
|
||||||
expect(pickedServers.length).toBe(1);
|
expect(pickedServers.length).toBe(0);
|
||||||
expect(pickedServers[0]).toBe("[::1]:8448");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with hostnames with ports', function() {
|
it('should work with hostnames with ports', function() {
|
||||||
|
@ -235,6 +263,140 @@ describe('matrix-to', function() {
|
||||||
expect(pickedServers[0]).toBe("example.org:8448");
|
expect(pickedServers[0]).toBe("example.org:8448");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not consider servers explicitly denied by ACLs', function() {
|
||||||
|
peg.get().getRoom = () => {
|
||||||
|
return {
|
||||||
|
getJoinedMembers: () => [
|
||||||
|
{
|
||||||
|
userId: "@alice:evilcorp.com",
|
||||||
|
powerLevel: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@bob:chat.evilcorp.com",
|
||||||
|
powerLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (type, key) => {
|
||||||
|
if (type !== "m.room.server_acl" || key !== "") return null;
|
||||||
|
return {
|
||||||
|
getContent: () => {
|
||||||
|
return {
|
||||||
|
deny: ["evilcorp.com", "*.evilcorp.com"],
|
||||||
|
allow: ["*"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
|
expect(pickedServers).toExist();
|
||||||
|
expect(pickedServers.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not consider servers not allowed by ACLs', function() {
|
||||||
|
peg.get().getRoom = () => {
|
||||||
|
return {
|
||||||
|
getJoinedMembers: () => [
|
||||||
|
{
|
||||||
|
userId: "@alice:evilcorp.com",
|
||||||
|
powerLevel: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@bob:chat.evilcorp.com",
|
||||||
|
powerLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (type, key) => {
|
||||||
|
if (type !== "m.room.server_acl" || key !== "") return null;
|
||||||
|
return {
|
||||||
|
getContent: () => {
|
||||||
|
return {
|
||||||
|
deny: [],
|
||||||
|
allow: [], // implies "ban everyone"
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
|
expect(pickedServers).toExist();
|
||||||
|
expect(pickedServers.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should consider servers not explicitly banned by ACLs', function() {
|
||||||
|
peg.get().getRoom = () => {
|
||||||
|
return {
|
||||||
|
getJoinedMembers: () => [
|
||||||
|
{
|
||||||
|
userId: "@alice:evilcorp.com",
|
||||||
|
powerLevel: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@bob:chat.evilcorp.com",
|
||||||
|
powerLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (type, key) => {
|
||||||
|
if (type !== "m.room.server_acl" || key !== "") return null;
|
||||||
|
return {
|
||||||
|
getContent: () => {
|
||||||
|
return {
|
||||||
|
deny: ["*.evilcorp.com"], // evilcorp.com is still good though
|
||||||
|
allow: ["*"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
|
expect(pickedServers).toExist();
|
||||||
|
expect(pickedServers.length).toBe(1);
|
||||||
|
expect(pickedServers[0]).toEqual("evilcorp.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should consider servers not disallowed by ACLs', function() {
|
||||||
|
peg.get().getRoom = () => {
|
||||||
|
return {
|
||||||
|
getJoinedMembers: () => [
|
||||||
|
{
|
||||||
|
userId: "@alice:evilcorp.com",
|
||||||
|
powerLevel: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@bob:chat.evilcorp.com",
|
||||||
|
powerLevel: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (type, key) => {
|
||||||
|
if (type !== "m.room.server_acl" || key !== "") return null;
|
||||||
|
return {
|
||||||
|
getContent: () => {
|
||||||
|
return {
|
||||||
|
deny: [],
|
||||||
|
allow: ["evilcorp.com"], // implies "ban everyone else"
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const pickedServers = pickServerCandidates("!somewhere:example.org");
|
||||||
|
expect(pickedServers).toExist();
|
||||||
|
expect(pickedServers.length).toBe(1);
|
||||||
|
expect(pickedServers[0]).toEqual("evilcorp.com");
|
||||||
|
});
|
||||||
|
|
||||||
it('should generate an event permalink for room IDs with no candidate servers', function() {
|
it('should generate an event permalink for room IDs with no candidate servers', function() {
|
||||||
peg.get().getRoom = () => null;
|
peg.get().getRoom = () => null;
|
||||||
const result = makeEventPermalink("!somewhere:example.org", "$something:example.com");
|
const result = makeEventPermalink("!somewhere:example.org", "$something:example.com");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue