Merge branches 'develop' and 't3chguy/cs_verification_decoration' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/cs_verification_decoration

This commit is contained in:
Michael Telatynski 2020-01-28 17:19:59 +00:00
commit 76e61b9948
38 changed files with 1195 additions and 254 deletions

View file

@ -2,19 +2,16 @@ module.exports = {
"sourceMaps": "inline",
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": [
"last 2 versions"
]
},
"modules": "commonjs"
"targets": [
"last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions"
],
}],
"@babel/preset-typescript",
"@babel/preset-flow",
"@babel/preset-react"
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-decorators", {legacy: true}],
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",

View file

@ -80,7 +80,7 @@
"is-ip": "^2.0.0",
"linkifyjs": "^2.1.6",
"lodash": "^4.17.14",
"matrix-js-sdk": "4.0.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"pako": "^1.0.5",
"png-chunks-extract": "^1.0.0",
"prop-types": "^15.5.8",

View file

@ -428,6 +428,11 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
color: $accent-fg-color;
}
.mx_Dialog button.warning, .mx_Dialog input[type="submit"].warning {
border: solid 1px $warning-color;
color: $warning-color;
}
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:disabled, .mx_Dialog_buttons input[type="submit"]:disabled {
background-color: $light-fg-color;
border: solid 1px $light-fg-color;

View file

@ -36,6 +36,7 @@
@import "./views/auth/_AuthHeader.scss";
@import "./views/auth/_AuthHeaderLogo.scss";
@import "./views/auth/_AuthPage.scss";
@import "./views/auth/_CompleteSecurityBody.scss";
@import "./views/auth/_CountryDropdown.scss";
@import "./views/auth/_InteractiveAuthEntryComponents.scss";
@import "./views/auth/_LanguageSelector.scss";
@ -65,7 +66,9 @@
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
@import "./views/dialogs/_RoomUpgradeDialog.scss";
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
@import "./views/dialogs/_SetEmailDialog.scss";
@ -150,10 +153,10 @@
@import "./views/rooms/_AuxPanel.scss";
@import "./views/rooms/_BasicMessageComposer.scss";
@import "./views/rooms/_E2EIcon.scss";
@import "./views/rooms/_InviteOnlyIcon.scss";
@import "./views/rooms/_EditMessageComposer.scss";
@import "./views/rooms/_EntityTile.scss";
@import "./views/rooms/_EventTile.scss";
@import "./views/rooms/_InviteOnlyIcon.scss";
@import "./views/rooms/_JumpToBottomButton.scss";
@import "./views/rooms/_LinkPreviewWidget.scss";
@import "./views/rooms/_MemberDeviceInfo.scss";

View file

@ -22,7 +22,7 @@ limitations under the License.
.mx_CompleteSecurity_headerIcon {
width: 24px;
height: 24px;
margin: 0 4px;
margin-right: 4px;
position: relative;
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
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.
@ -15,6 +16,9 @@ limitations under the License.
*/
.mx_AuthBody {
width: 500px;
font-size: 12px;
color: $authpage-secondary-color;
background-color: $authpage-body-bg-color;
border-radius: 0 4px 4px 0;
padding: 25px 60px;
@ -92,16 +96,6 @@ limitations under the License.
}
}
.mx_AuthBody_noHeader {
border-radius: 4px;
}
.mx_AuthBody_loginRegister {
width: 500px;
font-size: 12px;
color: $authpage-secondary-color;
}
.mx_AuthBody_editServerDetails {
padding-left: 1em;
font-size: 12px;

View file

@ -0,0 +1,42 @@
/*
Copyright 2019 New Vector Ltd
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_CompleteSecurityBody {
width: 600px;
color: $authpage-primary-color;
background-color: $authpage-body-bg-color;
border-radius: 4px;
padding: 20px;
box-sizing: border-box;
h2 {
font-size: 24px;
font-weight: 600;
margin-top: 0;
}
h3 {
font-size: 14px;
font-weight: 600;
}
a:link,
a:hover,
a:visited {
@mixin mx_Dialog_link;
}
}

View file

@ -0,0 +1,37 @@
/*
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_NewSessionReviewDialog_header {
display: flex;
align-items: center;
margin-top: 0;
}
.mx_NewSessionReviewDialog_headerIcon {
width: 24px;
height: 24px;
margin-right: 4px;
position: relative;
}
.mx_NewSessionReviewDialog_deviceName {
font-weight: 600;
}
.mx_NewSessionReviewDialog_deviceID {
font-size: 12px;
color: $notice-secondary-color;
}

View file

@ -56,16 +56,3 @@ limitations under the License.
mask-position: center;
}
.mx_RoomSettingsDialog_BridgeList {
padding: 0;
}
.mx_RoomSettingsDialog_BridgeList li {
list-style-type: none;
padding: 5px;
margin-bottom: 5px;
border-width: 1px 0px;
border-color: #dee1f3;
border-style: solid;
}

View file

@ -0,0 +1,112 @@
/*
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_RoomSettingsDialog_BridgeList {
padding: 0;
.mx_AccessibleButton {
display: inline;
margin: 0;
padding: 0;
}
}
.mx_RoomSettingsDialog_BridgeList li {
list-style-type: none;
padding: 5px;
margin-bottom: 8px;
border-width: 1px 1px;
border-color: $primary-hairline-color;
border-style: solid;
border-radius: 5px;
.column-icon {
float: left;
padding-right: 10px;
* {
border-radius: 5px;
border: 1px solid $input-darker-bg-color;
}
.noProtocolIcon {
width: 48px;
height: 48px;
background: $input-darker-bg-color;
border-radius: 5px;
}
.protocol-icon {
float: left;
margin-right: 5px;
img {
border-radius: 5px;
border-width: 1px 1px;
border-color: $primary-hairline-color;
}
span {
/* Correct letter placement */
left: auto;
}
}
}
.column-data {
display: inline-block;
width: 85%;
> h3 {
margin-top: 0px;
margin-bottom: 0px;
font-size: 16pt;
color: $primary-fg-color;
}
> * {
margin-top: 4px;
margin-bottom: 0;
}
.workspace-channel-details {
color: $primary-fg-color;
font-weight: 600;
.channel {
margin-left: 5px;
}
}
.mx_showMore {
display: block;
text-align: left;
margin-top: 10px;
}
.metadata {
color: $muted-fg-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0;
}
.metadata.visible {
overflow-y: visible;
text-overflow: ellipsis;
white-space: normal;
}
}
}

View file

@ -20,6 +20,7 @@ class Skinner {
}
getComponent(name) {
if (!name) throw new Error(`Invalid component name: ${name}`);
if (this.components === null) {
throw new Error(
"Attempted to get a component before a skin has been loaded."+
@ -41,13 +42,7 @@ class Skinner {
};
// Check the skin first
let comp = doLookup(this.components);
// If that failed, check against our own components
if (!comp) {
// Lazily load our own components because they might end up calling .getComponent()
comp = doLookup(require("./component-index").components);
}
const comp = doLookup(this.components);
// Just return nothing instead of erroring - the consumer should be smart enough to
// handle this at this point.
@ -75,6 +70,13 @@ class Skinner {
const comp = skinObject.components[compKeys[i]];
this.addComponent(compKeys[i], comp);
}
// Now that we have a skin, load our components too
const idx = require("./component-index");
if (!idx || !idx.components) throw new Error("Invalid react-sdk component index");
for (const c in idx.components) {
if (!this.components[c]) this.components[c] = idx.components[c];
}
}
addComponent(name, comp) {

View file

@ -0,0 +1,73 @@
/*
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 * as sdk from '../../../../index';
import PropTypes from 'prop-types';
import dis from "../../../../dispatcher";
import { _t } from '../../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
/*
* Allows the user to disable the Event Index.
*/
export default class DisableEventIndexDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
disabling: false,
};
}
_onDisable = async () => {
this.setState({
disabling: true,
});
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex();
this.props.onFinished();
dis.dispatch({ action: 'view_user_settings' });
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
{_t("If disabled, messages from encrypted rooms won't appear in search results.")}
{this.state.disabling ? <Spinner /> : <div />}
<DialogButtons
primaryButton={_t('Disable')}
onPrimaryButtonClick={this._onDisable}
primaryButtonClass="danger"
cancelButtonClass="warning"
onCancel={this.props.onFinished}
disabled={this.state.disabling}
/>
</BaseDialog>
);
}
}

View file

@ -0,0 +1,154 @@
/*
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 * as sdk from '../../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils";
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
/*
* Allows the user to introspect the event index state and disable it.
*/
export default class ManageEventIndexDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
eventIndexSize: 0,
eventCount: 0,
roomCount: 0,
currentRoom: null,
};
}
async updateCurrentRoom(room) {
const eventIndex = EventIndexPeg.get();
const stats = await eventIndex.getStats();
let currentRoom = null;
if (room) currentRoom = room.name;
this.setState({
eventIndexSize: stats.size,
roomCount: stats.roomCount,
eventCount: stats.eventCount,
currentRoom: currentRoom,
});
}
componentWillUnmount(): void {
const eventIndex = EventIndexPeg.get();
if (eventIndex !== null) {
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom.bind(this));
}
}
async componentWillMount(): void {
let eventIndexSize = 0;
let roomCount = 0;
let eventCount = 0;
let currentRoom = null;
const eventIndex = EventIndexPeg.get();
if (eventIndex !== null) {
eventIndex.on("changedCheckpoint", this.updateCurrentRoom.bind(this));
const stats = await eventIndex.getStats();
eventIndexSize = stats.size;
roomCount = stats.roomCount;
eventCount = stats.eventCount;
const room = eventIndex.currentRoom();
if (room) currentRoom = room.name;
}
this.setState({
eventIndexSize,
eventCount,
roomCount,
currentRoom,
});
}
_onDisable = async () => {
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
import("./DisableEventIndexDialog"),
null, null, /* priority = */ false, /* static = */ true,
);
}
_onDone = () => {
this.props.onFinished(true);
}
render() {
let crawlerState;
if (this.state.currentRoom === null) {
crawlerState = _t("Not currently downloading messages for any room.");
} else {
crawlerState = (
_t("Downloading mesages for %(currentRoom)s.", { currentRoom: this.state.currentRoom })
);
}
const eventIndexingSettings = (
<div>
{
_t( "Riot is securely caching encrypted messages locally for them " +
"to appear in search results:",
)
}
<div className='mx_SettingsTab_subsectionText'>
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
{_t("Number of rooms:")} {formatCountLong(this.state.roomCount)}<br />
{crawlerState}<br />
</div>
</div>
);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_ManageEventIndexDialog'
onFinished={this.props.onFinished}
title={_t("Message search")}
>
{eventIndexingSettings}
<DialogButtons
primaryButton={_t("Done")}
onPrimaryButtonClick={this.props.onFinished}
primaryButtonClass="primary"
cancelButton={_t("Disable")}
onCancel={this._onDisable}
cancelButtonClass="danger"
/>
</BaseDialog>
);
}
}

View file

@ -1834,6 +1834,7 @@ export default createReactClass({
this._accountPassword = null;
this._accountPasswordTimer = null;
}, 60 * 5 * 1000);
// Wait for the client to be logged in (but not started)
// which is enough to ask the server about account data.
const loggedIn = new Promise(resolve => {
@ -1867,6 +1868,9 @@ export default createReactClass({
}
if (masterKeyInStorage) {
// Auto-enable cross-signing for the new session when key found in
// secret storage.
SettingsStore.setFeatureEnabled("feature_cross_signing", true);
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
} else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
// This will only work if the feature is set to 'enable' in the config,

View file

@ -811,7 +811,7 @@ export default createReactClass({
debuglog("e2e verified", verified, "unverified", unverified);
/* Check all verified user devices. */
for (const userId of verified) {
for (const userId of [...verified, cli.getUserId()]) {
const devices = await cli.getStoredDevicesForUser(userId);
const anyDeviceNotVerified = devices.some(({deviceId}) => {
return !cli.checkDeviceTrust(userId, deviceId).isVerified();

View file

@ -112,7 +112,7 @@ export default class CompleteSecurity extends React.Component {
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthBody = sdk.getComponent("auth.AuthBody");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
@ -204,7 +204,7 @@ export default class CompleteSecurity extends React.Component {
return (
<AuthPage>
<AuthBody header={false}>
<CompleteSecurityBody>
<h2 className="mx_CompleteSecurity_header">
{icon}
{title}
@ -212,7 +212,7 @@ export default class CompleteSecurity extends React.Component {
<div className="mx_CompleteSecurity_body">
{body}
</div>
</AuthBody>
</CompleteSecurityBody>
</AuthPage>
);
}

View file

@ -34,16 +34,16 @@ export default class E2eSetup extends React.Component {
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthBody = sdk.getComponent("auth.AuthBody");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
return (
<AuthPage>
<AuthBody header={false}>
<CompleteSecurityBody>
<AsyncWrapper prom={this._createStorageDialogPromise}
hasCancel={false}
onFinished={this.props.onFinished}
accountPassword={this.props.accountPassword}
/>
</AuthBody>
</CompleteSecurityBody>
</AuthPage>
);
}

View file

@ -17,29 +17,10 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
export default class AuthBody extends React.PureComponent {
static PropTypes = {
header: PropTypes.bool,
};
static defaultProps = {
header: true,
};
render() {
const classes = {
'mx_AuthBody': true,
'mx_AuthBody_noHeader': !this.props.header,
// XXX The login pages all use a smaller fonts size but we don't want this
// for subsequent auth screens like the e2e setup. Doing this a terrible way
// for now.
'mx_AuthBody_loginRegister': this.props.header,
};
return <div className={classnames(classes)}>
return <div className="mx_AuthBody">
{ this.props.children }
</div>;
}

View file

@ -62,7 +62,7 @@ export default createReactClass({
console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
let protocol = global.location.protocol;
if (protocol === "vector:") {
if (protocol !== "http:") {
protocol = "https:";
}
const scriptTag = document.createElement('script');

View file

@ -0,0 +1,27 @@
/*
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.
*/
'use strict';
import React from 'react';
export default class CompleteSecurityBody extends React.PureComponent {
render() {
return <div className="mx_CompleteSecurityBody">
{ this.props.children }
</div>;
}
}

View file

@ -0,0 +1,91 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { replaceableComponent } from '../../../utils/replaceableComponent';
import DeviceVerifyDialog from './DeviceVerifyDialog';
import BaseDialog from './BaseDialog';
import DialogButtons from '../elements/DialogButtons';
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
export default class NewSessionReviewDialog extends React.PureComponent {
static propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
}
onCancelClick = () => {
this.props.onFinished(false);
}
onContinueClick = () => {
const { userId, device } = this.props;
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', DeviceVerifyDialog, {
userId,
device,
}, null, /* priority = */ false, /* static = */ true);
}
render() {
const { device } = this.props;
const icon = <span className="mx_NewSessionReviewDialog_headerIcon mx_E2EIcon_warning"></span>;
const titleText = _t("New session");
const title = <h2 className="mx_NewSessionReviewDialog_header">
{icon}
{titleText}
</h2>;
return (
<BaseDialog
title={title}
onFinished={this.props.onFinished}
>
<div className="mx_NewSessionReviewDialog_body">
<p>{_t(
"Use this session to verify your new one, " +
"granting it access to encrypted messages:",
)}</p>
<div className="mx_NewSessionReviewDialog_deviceInfo">
<div>
<span className="mx_NewSessionReviewDialog_deviceName">
{device.getDisplayName()}
</span> <span className="mx_NewSessionReviewDialog_deviceID">
({device.deviceId})
</span>
</div>
</div>
<p>{_t(
"If you didnt sign in to this session, " +
"your account may be compromised.",
)}</p>
<DialogButtons
cancelButton={_t("This wasn't me")}
cancelButtonClass="danger"
primaryButton={_t("Continue")}
onCancel={this.onCancelClick}
onPrimaryButtonClick={this.onContinueClick}
/>
</div>
</BaseDialog>
);
}
}

View file

@ -54,9 +54,6 @@ export default class RoomSettingsDialog extends React.Component {
_getTabs() {
const tabs = [];
const featureFlag = SettingsStore.isFeatureEnabled("feature_bridge_state");
const shouldShowBridgeIcon = featureFlag &&
BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0;
tabs.push(new Tab(
_td("General"),
@ -79,9 +76,9 @@ export default class RoomSettingsDialog extends React.Component {
<NotificationSettingsTab roomId={this.props.roomId} />,
));
if (shouldShowBridgeIcon) {
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) {
tabs.push(new Tab(
_td("Bridge Info"),
_td("Bridges"),
"mx_RoomSettingsDialog_bridgesIcon",
<BridgeSettingsTab roomId={this.props.roomId} />,
));

View file

@ -43,6 +43,10 @@ export default createReactClass({
// should there be a cancel button? default: true
hasCancel: PropTypes.bool,
// The class of the cancel button, only used if a cancel button is
// enabled
cancelButtonClass: PropTypes.node,
// onClick handler for the cancel button.
onCancel: PropTypes.func,
@ -72,12 +76,14 @@ export default createReactClass({
primaryButtonClassName += " " + this.props.primaryButtonClass;
}
let cancelButton;
if (this.props.cancelButton || this.props.hasCancel) {
cancelButton = <button
// important: the default type is 'submit' and this button comes before the
// primary in the DOM so will get form submissions unless we make it not a submit.
type="button"
onClick={this._onCancelClick}
className={this.props.cancelButtonClass}
disabled={this.props.disabled}
>
{ this.props.cancelButton || _t("Cancel") }

View file

@ -166,7 +166,7 @@ export default createReactClass({
});
/* Check all verified user devices. */
for (const userId of verified) {
for (const userId of [...verified, cli.getUserId()]) {
const devices = await cli.getStoredDevicesForUser(userId);
const allDevicesVerified = devices.every(({deviceId}) => {
return cli.checkDeviceTrust(userId, deviceId).isVerified();

View file

@ -0,0 +1,114 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {_t} from "../../../languageHandler";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import Pill from "../elements/Pill";
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import BaseAvatar from "../avatars/BaseAvatar";
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.settings.BridgeTile")
export default class BridgeTile extends React.PureComponent {
static propTypes = {
ev: PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
}
state = {
visible: false,
}
_toggleVisible() {
this.setState({
visible: !this.state.visible,
});
}
render() {
const content = this.props.ev.getContent();
const { channel, network, protocol } = content;
const protocolName = protocol.displayname || protocol.id;
const channelName = channel.displayname || channel.id;
const networkName = network ? network.displayname || network.id : protocolName;
let creator = null;
if (content.creator) {
creator = _t("This bridge was provisioned by <user />.", {}, {
user: <Pill
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(content.creator)}
shouldShowPillAvatar={true}
/>,
});
}
const bot = _t("This bridge is managed by <user />.", {}, {
user: <Pill
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(this.props.ev.getSender())}
shouldShowPillAvatar={true}
/>,
});
let networkIcon;
if (protocol.avatar) {
const avatarUrl = getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
protocol.avatar, 64, 64, "crop",
);
networkIcon = <BaseAvatar className="protocol-icon"
width={48}
height={48}
resizeMethod='crop'
name={ protocolName }
idName={ protocolName }
url={ avatarUrl }
/>;
} else {
networkIcon = <div class="noProtocolIcon"></div>;
}
const id = this.props.ev.getId();
const metadataClassname = "metadata" + (this.state.visible ? " visible" : "");
return (<li key={id}>
<div className="column-icon">
{networkIcon}
</div>
<div className="column-data">
<h3>{protocolName}</h3>
<p className="workspace-channel-details">
<span>{_t("Workspace: %(networkName)s", {networkName})}</span>
<span className="channel">{_t("Channel: %(channelName)s", {channelName})}</span>
</p>
<p className={metadataClassname}>
{creator} {bot}
</p>
<AccessibleButton className="mx_showMore" kind="secondary" onClick={this._toggleVisible.bind(this)}>
{ this.state.visible ? _t("Show less") : _t("Show more") }
</AccessibleButton>
</div>
</li>);
}
}

View file

@ -0,0 +1,187 @@
/*
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 { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import {formatBytes, formatCountLong} from "../../../utils/FormattingUtils";
import EventIndexPeg from "../../../indexing/EventIndexPeg";
export default class EventIndexPanel extends React.Component {
constructor() {
super();
this.state = {
enabling: false,
eventIndexSize: 0,
roomCount: 0,
eventIndexingEnabled:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'enableEventIndexing'),
};
}
async updateCurrentRoom(room) {
const eventIndex = EventIndexPeg.get();
const stats = await eventIndex.getStats();
this.setState({
eventIndexSize: stats.size,
roomCount: stats.roomCount,
});
}
componentWillUnmount(): void {
const eventIndex = EventIndexPeg.get();
if (eventIndex !== null) {
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom.bind(this));
}
}
async componentWillMount(): void {
this.updateState();
}
async updateState() {
const eventIndex = EventIndexPeg.get();
const eventIndexingEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, 'enableEventIndexing');
const enabling = false;
let eventIndexSize = 0;
let roomCount = 0;
if (eventIndex !== null) {
eventIndex.on("changedCheckpoint", this.updateCurrentRoom.bind(this));
const stats = await eventIndex.getStats();
eventIndexSize = stats.size;
roomCount = stats.roomCount;
}
this.setState({
enabling,
eventIndexSize,
roomCount,
eventIndexingEnabled,
});
}
_onManage = async () => {
Modal.createTrackedDialogAsync('Message search', 'Message search',
import('../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog'),
{
onFinished: () => {},
}, null, /* priority = */ false, /* static = */ true,
);
}
_onEnable = async () => {
this.setState({
enabling: true,
});
await EventIndexPeg.initEventIndex();
await EventIndexPeg.get().addInitialCheckpoints();
await EventIndexPeg.get().startCrawler();
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, true);
await this.updateState();
}
render() {
let eventIndexingSettings = null;
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
if (EventIndexPeg.get() !== null) {
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>
{_t( "Securely cache encrypted messages locally for them " +
"to appear in search results, using ")
} {formatBytes(this.state.eventIndexSize, 0)}
{_t( " to store messages from ")}
{formatCountLong(this.state.roomCount)} {_t("rooms.")}
</div>
<div>
<AccessibleButton kind="primary" onClick={this._onManage}>
{_t("Manage")}
</AccessibleButton>
</div>
</div>
);
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>
{_t( "Securely cache encrypted messages locally for them to " +
"appear in search results.")}
</div>
<div>
<AccessibleButton kind="primary" disabled={this.state.enabling}
onClick={this._onEnable}>
{_t("Enable")}
</AccessibleButton>
{this.state.enabling ? <InlineSpinner /> : <div />}
</div>
</div>
);
} else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
const nativeLink = (
"https://github.com/vector-im/riot-web/blob/develop/" +
"docs/native-node-modules.md#" +
"adding-seshat-for-search-in-e2e-encrypted-rooms"
);
eventIndexingSettings = (
<div>
{
_t( "Riot is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
"experiment with this feature, build a custom Riot Desktop " +
"with <nativeLink>search components added</nativeLink>.",
{},
{
'nativeLink': (sub) => <a href={nativeLink} target="_blank"
rel="noopener">{sub}</a>,
},
)
}
</div>
);
} else {
eventIndexingSettings = (
<div>
{
_t( "Riot can't securely cache encrypted messages locally " +
"while running in a web browser. Use <riotLink>Riot Desktop</riotLink> " +
"for encrypted messages to appear in search results.",
{},
{
'riotLink': (sub) => <a href="https://riot.im/download/desktop"
target="_blank" rel="noopener">{sub}</a>,
},
)
}
</div>
);
}
return eventIndexingSettings;
}
}

View file

@ -18,16 +18,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import Pill from "../../../elements/Pill";
import {makeUserPermalink} from "../../../../../utils/permalinks/Permalinks";
import BaseAvatar from "../../../avatars/BaseAvatar";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import BridgeTile from "../../BridgeTile";
const BRIDGE_EVENT_TYPES = [
"uk.half-shot.bridge",
// m.bridge
];
const BRIDGES_LINK = "https://matrix.org/bridges/";
export default class BridgeSettingsTab extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
@ -38,99 +37,7 @@ export default class BridgeSettingsTab extends React.Component {
if (!content || !content.channel || !content.protocol) {
return null;
}
const { channel, network } = content;
const protocolName = content.protocol.displayname || content.protocol.id;
const channelName = channel.displayname || channel.id;
const networkName = network ? network.displayname || network.id : protocolName;
let creator = null;
if (content.creator) {
creator = <p> { _t("This bridge was provisioned by <user />", {}, {
user: <Pill
type={Pill.TYPE_USER_MENTION}
room={room}
url={makeUserPermalink(content.creator)}
shouldShowPillAvatar={true}
/>,
})}</p>;
}
const bot = (<p> {_t("This bridge is managed by <user />.", {}, {
user: <Pill
type={Pill.TYPE_USER_MENTION}
room={room}
url={makeUserPermalink(event.getSender())}
shouldShowPillAvatar={true}
/>,
})} </p>);
let channelLink = channelName;
if (channel.external_url) {
channelLink = <a target="_blank" href={channel.external_url} rel="noopener">{channelName}</a>;
}
let networkLink = networkName;
if (network && network.external_url) {
networkLink = <a target="_blank" href={network.external_url} rel="noopener">{networkName}</a>;
}
const chanAndNetworkInfo = (
_t("Bridged into <channelLink /> <networkLink />, on <protocolName />", {}, {
channelLink,
networkLink,
protocolName,
})
);
let networkIcon = null;
if (networkName && network.avatar) {
const avatarUrl = getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
network.avatar, 32, 32, "crop",
);
networkIcon = <BaseAvatar
width={32}
height={32}
resizeMethod='crop'
name={ networkName }
idName={ networkName }
url={ avatarUrl }
/>;
}
let channelIcon = null;
if (channel.avatar) {
const avatarUrl = getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
channel.avatar, 32, 32, "crop",
);
channelIcon = <BaseAvatar
width={32}
height={32}
resizeMethod='crop'
name={ networkName }
idName={ networkName }
url={ avatarUrl }
/>;
}
const heading = _t("Connected to <channelIcon /> <channelName /> on <networkIcon /> <networkName />", { }, {
channelIcon,
channelName,
networkName,
networkIcon,
});
return (<li key={event.stateKey}>
<div>
<h3>{heading}</h3>
<p>{_t("Connected via %(protocolName)s", { protocolName })}</p>
<details>
{creator}
{bot}
<p>{chanAndNetworkInfo}</p>
</details>
</div>
</li>);
return <BridgeTile room={room} ev={event}></BridgeTile>;
}
static getBridgeStateEvents(roomId) {
@ -151,14 +58,40 @@ export default class BridgeSettingsTab extends React.Component {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
let content = null;
if (bridgeEvents.length > 0) {
content = <div>
<p>{_t(
"This room is bridging messages to the following platforms. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noopener">{sub}</a>,
},
)}</p>
<ul className="mx_RoomSettingsDialog_BridgeList">
{ bridgeEvents.map((event) => this._renderBridgeCard(event, room)) }
</ul>
</div>;
} else {
content = <p>{_t(
"This room isnt bridging messages to any platforms. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noopener">{sub}</a>,
},
)}</p>;
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Bridge Info")}</div>
<div className="mx_SettingsTab_heading">{_t("Bridges")}</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<p>{ _t("Below is a list of bridges connected to this room.") }</p>
<ul className="mx_RoomSettingsDialog_BridgeList">
{ bridgeEvents.map((event) => this._renderBridgeCard(event, room)) }
</ul>
{content}
</div>
</div>
);

View file

@ -170,6 +170,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}

View file

@ -242,6 +242,7 @@ export default class SecurityUserSettingsTab extends React.Component {
render() {
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
const keyBackup = (
@ -253,6 +254,16 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>
);
let eventIndex;
if (SettingsStore.isFeatureEnabled("feature_event_indexing")) {
eventIndex = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
<EventIndexPanel />
</div>
);
}
// XXX: There's no such panel in the current cross-signing designs, but
// it's useful to have for testing the feature. If there's no interest
// in having advanced details here once all flows are implemented, we
@ -281,6 +292,7 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>
</div>
{keyBackup}
{eventIndex}
{crossSigning}
{this._renderCurrentDeviceInfo()}
<div className='mx_SettingsTab_section'>

View file

@ -16,12 +16,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import Modal from "../../../Modal";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener';
import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.VerifySessionToast")
export default class VerifySessionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
@ -34,18 +37,16 @@ export default class VerifySessionToast extends React.PureComponent {
_onReviewClick = async () => {
const cli = MatrixClientPeg.get();
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
const device = await cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
Modal.createTrackedDialog('New Session Verify', 'Starting dialog', DeviceVerifyDialog, {
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: MatrixClientPeg.get().getUserId(),
device,
}, null, /* priority = */ false, /* static = */ true);
};
render() {
const FormButton = sdk.getComponent("elements.FormButton");
return (<div>
<div className="mx_Toast_description">{_t("Review & verify your new session")}</div>
<div className="mx_Toast_buttons" aria-live="off">

View file

@ -414,6 +414,7 @@
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
"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",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading report": "Uploading report",
@ -526,6 +527,12 @@
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
"Upload": "Upload",
"Remove": "Remove",
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
"Workspace: %(networkName)s": "Workspace: %(networkName)s",
"Channel: %(channelName)s": "Channel: %(channelName)s",
"Show less": "Show less",
"Show more": "Show more",
"Failed to upload profile picture!": "Failed to upload profile picture!",
"Upload new:": "Upload new:",
"No display name": "No display name",
@ -562,6 +569,14 @@
"Failed to set display name": "Failed to set display name",
"Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ",
" to store messages from ": " to store messages from ",
"rooms.": "rooms.",
"Manage": "Manage",
"Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
"Enable": "Enable",
"Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.": "Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.",
"Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.": "Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.",
"Connecting to integration manager...": "Connecting to integration manager...",
"Cannot connect to integration manager": "Cannot connect to integration manager",
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
@ -755,6 +770,7 @@
"Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites",
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
"Key backup": "Key backup",
"Message search": "Message search",
"Cross-signing": "Cross-signing",
"Security & Privacy": "Security & Privacy",
"Devices": "Devices",
@ -785,13 +801,9 @@
"Room version:": "Room version:",
"Developer options": "Developer options",
"Open Devtools": "Open Devtools",
"This bridge was provisioned by <user />": "This bridge was provisioned by <user />",
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
"Bridged into <channelLink /> <networkLink />, on <protocolName />": "Bridged into <channelLink /> <networkLink />, on <protocolName />",
"Connected to <channelIcon /> <channelName /> on <networkIcon /> <networkName />": "Connected to <channelIcon /> <channelName /> on <networkIcon /> <networkName />",
"Connected via %(protocolName)s": "Connected via %(protocolName)s",
"Bridge Info": "Bridge Info",
"Below is a list of bridges connected to this room.": "Below is a list of bridges connected to this room.",
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "This room is bridging messages to the following platforms. <a>Learn more.</a>",
"This room isnt bridging messages to any platforms. <a>Learn more.</a>": "This room isnt bridging messages to any platforms. <a>Learn more.</a>",
"Bridges": "Bridges",
"Room Addresses": "Room Addresses",
"Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?",
"URL Previews": "URL Previews",
@ -1496,7 +1508,6 @@
"Recent Conversations": "Recent Conversations",
"Suggestions": "Suggestions",
"Recently Direct Messaged": "Recently Direct Messaged",
"Show more": "Show more",
"If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.": "If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.",
"Go": "Go",
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.",
@ -1519,6 +1530,10 @@
"Are you sure you want to sign out?": "Are you sure you want to sign out?",
"Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.",
"Message edits": "Message edits",
"New session": "New session",
"Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:",
"If you didnt sign in to this session, your account may be compromised.": "If you didnt sign in to this session, your account may be compromised.",
"This wasn't me": "This wasn't me",
"If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.",
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
"Report bugs & give feedback": "Report bugs & give feedback",
@ -2072,6 +2087,14 @@
"This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "This device has detected that your recovery passphrase and key for Secure Messages have been removed.",
"If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.",
"If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
"If disabled, messages from encrypted rooms won't appear in search results.": "If disabled, messages from encrypted rooms won't appear in search results.",
"Disable": "Disable",
"Not currently downloading messages for any room.": "Not currently downloading messages for any room.",
"Downloading mesages for %(currentRoom)s.": "Downloading mesages for %(currentRoom)s.",
"Riot is securely caching encrypted messages locally for them to appear in search results:": "Riot is securely caching encrypted messages locally for them to appear in search results:",
"Space used:": "Space used:",
"Indexed messages:": "Indexed messages:",
"Number of rooms:": "Number of rooms:",
"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 add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"

View file

@ -74,6 +74,12 @@ export interface LoadArgs {
direction: string;
}
export interface IndexStats {
size: number;
event_count: number;
room_count: number;
}
/**
* Base class for classes that provide platform-specific event indexing.
*
@ -124,6 +130,16 @@ export default class BaseEventIndexManager {
throw new Error("Unimplemented");
}
/**
* Get statistical information of the index.
*
* @return {Promise<IndexStats>} A promise that will resolve to the index
* statistics.
*/
async getStats(): Promise<IndexStats> {
throw new Error("Unimplemented");
}
/**
* Commit the previously queued up events to the index.
*

View file

@ -17,20 +17,25 @@ limitations under the License.
import PlatformPeg from "../PlatformPeg";
import {MatrixClientPeg} from "../MatrixClientPeg";
import {EventTimeline, RoomMember} from 'matrix-js-sdk';
import {sleep} from "../utils/promise";
import {EventEmitter} from "events";
/*
* Event indexing class that wraps the platform specific event indexing.
*/
export default class EventIndex {
export default class EventIndex extends EventEmitter {
constructor() {
super();
this.crawlerCheckpoints = [];
// The time that the crawler will wait between /rooms/{room_id}/messages
// requests
this._crawlerTimeout = 3000;
// The time in ms that the crawler will wait loop iterations if there
// have not been any checkpoints to consume in the last iteration.
this._crawlerIdleTime = 5000;
this._crawlerSleepTime = 3000;
// The maximum number of events our crawler should fetch in a single
// crawl.
this._eventsPerCrawl = 100;
this._crawler = null;
this._currentCheckpoint = null;
this.liveEventsForIndex = new Set();
}
@ -65,59 +70,62 @@ export default class EventIndex {
client.removeListener('Room.timelineReset', this.onTimelineReset);
}
/**
* Get crawler checkpoints for the encrypted rooms and store them in the index.
*/
async addInitialCheckpoints() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
const client = MatrixClientPeg.get();
const rooms = client.getRooms();
const isRoomEncrypted = (room) => {
return client.isRoomEncrypted(room.roomId);
};
// We only care to crawl the encrypted rooms, non-encrypted
// rooms can use the search provided by the homeserver.
const encryptedRooms = rooms.filter(isRoomEncrypted);
console.log("EventIndex: Adding initial crawler checkpoints");
// Gather the prev_batch tokens and create checkpoints for
// our message crawler.
await Promise.all(encryptedRooms.map(async (room) => {
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
console.log("EventIndex: Got token for indexer",
room.roomId, token);
const backCheckpoint = {
roomId: room.roomId,
token: token,
direction: "b",
};
const forwardCheckpoint = {
roomId: room.roomId,
token: token,
direction: "f",
};
await indexManager.addCrawlerCheckpoint(backCheckpoint);
await indexManager.addCrawlerCheckpoint(forwardCheckpoint);
this.crawlerCheckpoints.push(backCheckpoint);
this.crawlerCheckpoints.push(forwardCheckpoint);
}));
}
onSync = async (state, prevState, data) => {
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (prevState === "PREPARED" && state === "SYNCING") {
const addInitialCheckpoints = async () => {
const client = MatrixClientPeg.get();
const rooms = client.getRooms();
const isRoomEncrypted = (room) => {
return client.isRoomEncrypted(room.roomId);
};
// We only care to crawl the encrypted rooms, non-encrypted.
// rooms can use the search provided by the homeserver.
const encryptedRooms = rooms.filter(isRoomEncrypted);
console.log("EventIndex: Adding initial crawler checkpoints");
// Gather the prev_batch tokens and create checkpoints for
// our message crawler.
await Promise.all(encryptedRooms.map(async (room) => {
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
console.log("EventIndex: Got token for indexer",
room.roomId, token);
const backCheckpoint = {
roomId: room.roomId,
token: token,
direction: "b",
};
const forwardCheckpoint = {
roomId: room.roomId,
token: token,
direction: "f",
};
await indexManager.addCrawlerCheckpoint(backCheckpoint);
await indexManager.addCrawlerCheckpoint(forwardCheckpoint);
this.crawlerCheckpoints.push(backCheckpoint);
this.crawlerCheckpoints.push(forwardCheckpoint);
}));
};
// If our indexer is empty we're most likely running Riot the
// first time with indexing support or running it with an
// initial sync. Add checkpoints to crawl our encrypted rooms.
const eventIndexWasEmpty = await indexManager.isEventIndexEmpty();
if (eventIndexWasEmpty) await addInitialCheckpoints();
if (eventIndexWasEmpty) await this.addInitialCheckpoints();
// Start our crawler.
this.startCrawler();
return;
}
@ -182,13 +190,11 @@ export default class EventIndex {
indexManager.addEventToIndex(e, profile);
}
async crawlerFunc() {
// TODO either put this in a better place or find a library provided
// method that does this.
const sleep = async (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
emitNewCheckpoint() {
this.emit("changedCheckpoint", this.currentRoom());
}
async crawlerFunc() {
let cancelled = false;
console.log("EventIndex: Started crawler function");
@ -202,11 +208,27 @@ export default class EventIndex {
cancelled = true;
};
let idle = false;
while (!cancelled) {
// This is a low priority task and we don't want to spam our
// homeserver with /messages requests so we set a hefty timeout
// here.
await sleep(this._crawlerTimeout);
let sleepTime = this._crawlerSleepTime;
// Don't let the user configure a lower sleep time than 100 ms.
sleepTime = Math.max(sleepTime, 100);
if (idle) {
sleepTime = this._crawlerIdleTime;
}
if (this._currentCheckpoint !== null) {
this._currentCheckpoint = null;
this.emitNewCheckpoint();
}
await sleep(sleepTime);
console.log("EventIndex: Running the crawler loop.");
@ -219,9 +241,15 @@ export default class EventIndex {
/// There is no checkpoint available currently, one may appear if
// a sync with limited room timelines happens, so go back to sleep.
if (checkpoint === undefined) {
idle = true;
continue;
}
this._currentCheckpoint = checkpoint;
this.emitNewCheckpoint();
idle = false;
console.log("EventIndex: crawling using checkpoint", checkpoint);
// We have a checkpoint, let us fetch some messages, again, very
@ -241,6 +269,11 @@ export default class EventIndex {
continue;
}
if (cancelled) {
this.crawlerCheckpoints.push(checkpoint);
break;
}
if (res.chunk.length === 0) {
console.log("EventIndex: Done with the checkpoint", checkpoint);
// We got to the start/end of our timeline, lets just
@ -600,4 +633,29 @@ export default class EventIndex {
return paginationPromise;
}
async getStats() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.getStats();
}
/**
* Get the room that we are currently crawling.
*
* @returns {Room} A MatrixRoom that is being currently crawled, null
* if no room is currently being crawled.
*/
currentRoom() {
if (this._currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
return null;
}
const client = MatrixClientPeg.get();
if (this._currentCheckpoint !== null) {
return client.getRoom(this._currentCheckpoint.roomId);
} else {
return client.getRoom(this.crawlerCheckpoints[0].roomId);
}
}
}

View file

@ -21,17 +21,19 @@ limitations under the License.
import PlatformPeg from "../PlatformPeg";
import EventIndex from "../indexing/EventIndex";
import SettingsStore from '../settings/SettingsStore';
import SettingsStore, {SettingLevel} from '../settings/SettingsStore';
class EventIndexPeg {
constructor() {
this.index = null;
this._supportIsInstalled = false;
}
/**
* Create a new EventIndex and initialize it if the platform supports it.
* Initialize the EventIndexPeg and if event indexing is enabled initialize
* the event index.
*
* @return {Promise<bool>} A promise that will resolve to true if an
* @return {Promise<boolean>} A promise that will resolve to true if an
* EventIndex was successfully initialized, false otherwise.
*/
async init() {
@ -40,12 +42,33 @@ class EventIndexPeg {
}
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (!indexManager || await indexManager.supportsEventIndexing() !== true) {
console.log("EventIndex: Platform doesn't support event indexing,",
"not initializing.");
if (!indexManager) {
console.log("EventIndex: Platform doesn't support event indexing, not initializing.");
return false;
}
this._supportIsInstalled = await indexManager.supportsEventIndexing();
if (!this.supportIsInstalled()) {
console.log("EventIndex: Event indexing isn't installed for the platform, not initializing.");
return false;
}
if (!SettingsStore.getValueAt(SettingLevel.DEVICE, 'enableEventIndexing')) {
console.log("EventIndex: Event indexing is disabled, not initializing");
return false;
}
return this.initEventIndex();
}
/**
* Initialize the event index.
*
* @returns {boolean} True if the event index was succesfully initialized,
* false otherwise.
*/
async initEventIndex() {
const index = new EventIndex();
try {
@ -60,6 +83,29 @@ class EventIndexPeg {
return true;
}
/**
* Check if the current platform has support for event indexing.
*
* @return {boolean} True if it has support, false otherwise. Note that this
* does not mean that support is installed.
*/
platformHasSupport(): boolean {
return PlatformPeg.get().getEventIndexingManager() !== null;
}
/**
* Check if event indexing support is installed for the platfrom.
*
* Event indexing might require additional optional modules to be installed,
* this tells us if those are installed. Note that this should only be
* called after the init() method was called.
*
* @return {boolean} True if support is installed, false otherwise.
*/
supportIsInstalled(): boolean {
return this._supportIsInstalled;
}
/**
* Get the current event index.
*
@ -69,6 +115,11 @@ class EventIndexPeg {
return this.index;
}
start() {
if (this.index === null) return;
this.index.startCrawler();
}
stop() {
if (this.index === null) return;
this.index.stopCrawler();

View file

@ -480,4 +480,9 @@ export const SETTINGS = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: RIGHT_PANEL_PHASES.GroupMemberList,
},
"enableEventIndexing": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("Enable message search in encrypted rooms"),
default: true,
},
};

View file

@ -145,7 +145,7 @@ export default class SettingsStore {
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
};
console.log(`Starting watcher for ${settingName}@${roomId || '<null room>'}`);
console.log(`Starting watcher for ${settingName}@${roomId || '<null room>'} as ID ${watcherId}`);
SettingsStore._watchers[watcherId] = localizedCallback;
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
@ -159,8 +159,12 @@ export default class SettingsStore {
* to cancel.
*/
static unwatchSetting(watcherReference) {
if (!SettingsStore._watchers[watcherReference]) return;
if (!SettingsStore._watchers[watcherReference]) {
console.warn(`Ending non-existent watcher ID ${watcherReference}`);
return;
}
console.log(`Ending watcher ID ${watcherReference}`);
defaultWatchManager.unwatchSetting(SettingsStore._watchers[watcherReference]);
delete SettingsStore._watchers[watcherReference];
}

View file

@ -30,6 +30,31 @@ export function formatCount(count) {
return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S
}
/**
* Format a count showing the whole number but making it a bit more readable.
* e.g: 1000 => 1,000
*/
export function formatCountLong(count) {
const formatter = new Intl.NumberFormat();
return formatter.format(count)
}
/**
* format a size in bytes into a human readable form
* e.g: 1024 -> 1.00 KB
*/
export function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* format a key into groups of 4 characters, for easier visual inspection
*

View file

@ -5760,10 +5760,9 @@ mathml-tag-names@^2.0.1:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc"
integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw==
matrix-js-sdk@4.0.0:
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "4.0.0"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-4.0.0.tgz#c81bdc905af2ab1634527e5f542f2f15977d31cf"
integrity sha512-Xbe36xL443qtEBH4xk0k39JabolqZfloK7fwYGMb/PgWO26VOzvw94XWahnIr5w83oxBAF9nFmP+7EnPG6IHnA==
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/21e4c597d9633aef606871cf9ffffaf039142be3"
dependencies:
"@babel/runtime" "^7.8.3"
another-json "^0.2.0"