Merge branch 'develop' into travis/update-qr-code

This commit is contained in:
Travis Ralston 2020-01-29 11:47:59 +00:00
commit a8cfde72e1
10 changed files with 196 additions and 51 deletions

View file

@ -264,6 +264,9 @@ limitations under the License.
display: block;
margin: 16px 0;
}
button.mx_UserInfo_verify {
width: 100%; // FIXME get rid of this once we get rid of DialogButtons here
}
}
.mx_UserInfo.mx_UserInfo_smallAvatar {

View file

@ -22,8 +22,9 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { accessSecretStorage } from '../../../CrossSigningManager';
const PHASE_INTRO = 0;
const PHASE_DONE = 1;
const PHASE_CONFIRM_SKIP = 2;
const PHASE_BUSY = 1;
const PHASE_DONE = 2;
const PHASE_CONFIRM_SKIP = 3;
export default class CompleteSecurity extends React.Component {
static propTypes = {
@ -39,6 +40,7 @@ export default class CompleteSecurity extends React.Component {
// the presence of it insidicating that we're in 'verify mode'.
// Because of the latter, it lives in the state.
verificationRequest: null,
backupInfo: null,
};
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
}
@ -53,10 +55,16 @@ export default class CompleteSecurity extends React.Component {
}
onStartClick = async () => {
this.setState({
phase: PHASE_BUSY,
});
const cli = MatrixClientPeg.get();
const backupInfo = await cli.getKeyBackupVersion();
this.setState({backupInfo});
try {
await accessSecretStorage(async () => {
await cli.checkOwnCrossSigningTrust();
if (backupInfo) await cli.restoreKeyBackupWithSecretStorage(backupInfo);
});
if (cli.getCrossSigningId()) {
@ -66,6 +74,9 @@ export default class CompleteSecurity extends React.Component {
}
} catch (e) {
// this will throw if the user hits cancel, so ignore
this.setState({
phase: PHASE_INTRO,
});
}
}
@ -155,13 +166,21 @@ export default class CompleteSecurity extends React.Component {
} else if (phase === PHASE_DONE) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
title = _t("Session verified");
let message;
if (this.state.backupInfo) {
message = <p>{_t(
"Your new session is now verified. It has access to your " +
"encrypted messages, and other users will see it as trusted.",
)}</p>;
} else {
message = <p>{_t(
"Your new session is now verified. Other users will see it as trusted.",
)}</p>;
}
body = (
<div>
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
<p>{_t(
"Your new session is now verified. It has access to your " +
"encrypted messages, and other users will see it as trusted.",
)}</p>
{message}
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="primary"
@ -198,6 +217,11 @@ export default class CompleteSecurity extends React.Component {
</div>
</div>
);
} else if (phase === PHASE_BUSY) {
const Spinner = sdk.getComponent('views.elements.Spinner');
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = '';
body = <Spinner />;
} else {
throw new Error(`Unknown phase ${phase}`);
}

View file

@ -351,9 +351,20 @@ export default class InviteDialog extends React.PureComponent {
continue;
}
const lastEventTs = room.timeline && room.timeline.length
? room.timeline[room.timeline.length - 1].getTs()
: 0;
// Find the last timestamp for a message event
const searchTypes = ["m.room.message", "m.room.encrypted", "m.sticker"];
const maxSearchEvents = 20; // to prevent traversing history
let lastEventTs = 0;
if (room.timeline && room.timeline.length) {
for (let i = room.timeline.length - 1; i >= 0; i--) {
const ev = room.timeline[i];
if (searchTypes.includes(ev.getType())) {
lastEventTs = ev.getTs();
break;
}
if (room.timeline.length - i > maxSearchEvents) break;
}
}
if (!lastEventTs) {
// something weird is going on with this room
console.warn(`[Invite:Recents] ${userId} (${room.roomId}) has a weird last timestamp: ${lastEventTs}`);
@ -747,6 +758,12 @@ export default class InviteDialog extends React.PureComponent {
};
_onPaste = async (e) => {
if (this.state.filterText) {
// if the user has already typed something, just let them
// paste normally.
return;
}
// Prevent the text being pasted into the textarea
e.preventDefault();
@ -937,6 +954,7 @@ export default class InviteDialog extends React.PureComponent {
value={this.state.filterText}
ref={this._editorRef}
onPaste={this._onPaste}
autoFocus={true}
/>
);
return (

View file

@ -36,7 +36,7 @@ const EncryptionPanel = ({verificationRequest, member, onClose}) => {
setRequest(verificationRequest);
}, [verificationRequest]);
const [phase, setPhase] = useState(false);
const [phase, setPhase] = useState(undefined);
const changeHandler = useCallback(() => {
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
@ -71,7 +71,7 @@ const EncryptionPanel = ({verificationRequest, member, onClose}) => {
setRequest(verificationRequest);
}, [member.userId]);
const requested = request && phase === PHASE_REQUESTED;
const requested = request && (phase === PHASE_REQUESTED || phase === undefined);
if (!request || requested) {
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
} else {

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import PropTypes from "prop-types";
import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
@ -23,6 +24,8 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler";
import E2EIcon from "../rooms/E2EIcon";
import {
PHASE_UNSENT,
PHASE_REQUESTED,
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
@ -31,6 +34,20 @@ import {
import Spinner from "../elements/Spinner";
export default class VerificationPanel extends React.PureComponent {
static propTypes = {
request: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
phase: PropTypes.oneOf([
PHASE_UNSENT,
PHASE_REQUESTED,
PHASE_READY,
PHASE_STARTED,
PHASE_CANCELLED,
PHASE_DONE,
]).isRequired,
onClose: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {};
@ -147,11 +164,11 @@ export default class VerificationPanel extends React.PureComponent {
}
render() {
const {member} = this.props;
const {member, phase} = this.props;
const displayName = member.displayName || member.name || member.userId;
switch (this.props.phase) {
switch (phase) {
case PHASE_READY:
return this.renderQRPhase();
case PHASE_STARTED:
@ -174,6 +191,7 @@ export default class VerificationPanel extends React.PureComponent {
case PHASE_CANCELLED:
return this.renderCancelledPhase();
}
console.error("VerificationPanel unhandled phase:", phase);
return null;
}

View file

@ -1113,7 +1113,8 @@ export default createReactClass({
}
}
const avatarUrl = this.props.member.getMxcAvatarUrl();
const {member} = this.props;
const avatarUrl = member.avatarUrl || (member.getMxcAvatarUrl && member.getMxcAvatarUrl());
let avatarElement;
if (avatarUrl) {
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);

View file

@ -27,7 +27,8 @@ function capFirst(s) {
export default class VerificationShowSas extends React.Component {
static propTypes = {
displayName: PropTypes.string.isRequired,
pending: PropTypes.bool,
displayName: PropTypes.string, // required if pending is true
onDone: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
sas: PropTypes.object.isRequired,
@ -95,7 +96,7 @@ export default class VerificationShowSas extends React.Component {
confirm = <DialogButtons
primaryButton={_t("They match")}
onPrimaryButtonClick={this.onMatchClick}
primaryButtonClassName="mx_UserInfo_verify"
primaryButtonClass="mx_UserInfo_verify"
cancelButton={_t("They don't match")}
onCancel={this.props.onCancel}
cancelButtonClass="mx_UserInfo_verify"

View file

@ -1912,6 +1912,7 @@
"Start": "Start",
"Session verified": "Session verified",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Done": "Done",
"Without completing security on this device, it wont have access to encrypted messages.": "Without completing security on this device, it wont have access to encrypted messages.",
"Go Back": "Go Back",

View file

@ -105,7 +105,7 @@ export default class BaseEventIndexManager {
* @return {Promise} A promise that will resolve when the event index is
* initialized.
*/
async initEventIndex(): Promise<> {
async initEventIndex(): Promise<void> {
throw new Error("Unimplemented");
}
@ -146,15 +146,15 @@ export default class BaseEventIndexManager {
* @return {Promise} A promise that will resolve once the queued up events
* were added to the index.
*/
async commitLiveEvents(): Promise<> {
async commitLiveEvents(): Promise<void> {
throw new Error("Unimplemented");
}
/**
* Search the event index using the given term for matching events.
*
* @param {SearchArgs} searchArgs The search configuration sets what should
* be searched for and what should be contained in the search result.
* @param {SearchArgs} searchArgs The search configuration for the search,
* sets the search term and determines the search result contents.
*
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
* of search results once the search is done.
@ -197,7 +197,7 @@ export default class BaseEventIndexManager {
* @return {Promise} A promise that will resolve once the checkpoint has
* been stored.
*/
async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> {
async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> {
throw new Error("Unimplemented");
}
@ -210,7 +210,7 @@ export default class BaseEventIndexManager {
* @return {Promise} A promise that will resolve once the checkpoint has
* been removed.
*/
async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> {
async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> {
throw new Error("Unimplemented");
}
@ -250,7 +250,7 @@ export default class BaseEventIndexManager {
* @return {Promise} A promise that will resolve once the event index has
* been closed.
*/
async closeEventIndex(): Promise<> {
async closeEventIndex(): Promise<void> {
throw new Error("Unimplemented");
}
@ -260,7 +260,7 @@ export default class BaseEventIndexManager {
* @return {Promise} A promise that will resolve once the event index has
* been deleted.
*/
async deleteEventIndex(): Promise<> {
async deleteEventIndex(): Promise<void> {
throw new Error("Unimplemented");
}
}

View file

@ -51,6 +51,9 @@ export default class EventIndex extends EventEmitter {
this.registerListeners();
}
/**
* Register event listeners that are necessary for the event index to work.
*/
registerListeners() {
const client = MatrixClientPeg.get();
@ -60,6 +63,9 @@ export default class EventIndex extends EventEmitter {
client.on('Room.timelineReset', this.onTimelineReset);
}
/**
* Remove the event index specific event listeners.
*/
removeListeners() {
const client = MatrixClientPeg.get();
if (client === null) return;
@ -116,6 +122,15 @@ export default class EventIndex extends EventEmitter {
}));
}
/*
* The sync event listener.
*
* The listener has two cases:
* - First sync after start up, check if the index is empty, add
* initial checkpoints, if so. Start the crawler background task.
* - Every other sync, tell the event index to commit all the queued up
* live events
*/
onSync = async (state, prevState, data) => {
const indexManager = PlatformPeg.get().getEventIndexingManager();
@ -139,6 +154,14 @@ export default class EventIndex extends EventEmitter {
}
}
/*
* The Room.timeline listener.
*
* This listener waits for live events in encrypted rooms, if they are
* decrypted or unencrypted we queue them to be added to the index,
* otherwise we save their event id and wait for them in the Event.decrypted
* listener.
*/
onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
// We only index encrypted rooms locally.
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
@ -162,6 +185,12 @@ export default class EventIndex extends EventEmitter {
}
}
/*
* The Event.decrypted listener.
*
* Checks if the event was marked for addition in the Room.timeline
* listener, if so queues it up to be added to the index.
*/
onEventDecrypted = async (ev, err) => {
const eventId = ev.getId();
@ -171,6 +200,41 @@ export default class EventIndex extends EventEmitter {
await this.addLiveEventToIndex(ev);
}
/*
* The Room.timelineReset listener.
*
* Listens for timeline resets that are caused by a limited timeline to
* re-add checkpoints for rooms that need to be crawled again.
*/
onTimelineReset = async (room, timelineSet, resetAllTimelines) => {
if (room === null) return;
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
const backwardsCheckpoint = {
roomId: room.roomId,
token: token,
fullCrawl: false,
direction: "b",
};
console.log("EventIndex: Added checkpoint because of a limited timeline",
backwardsCheckpoint);
await indexManager.addCrawlerCheckpoint(backwardsCheckpoint);
this.crawlerCheckpoints.push(backwardsCheckpoint);
}
/**
* Queue up live events to be added to the event index.
*
* @param {MatrixEvent} ev The event that should be added to the index.
*/
async addLiveEventToIndex(ev) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
@ -190,10 +254,24 @@ export default class EventIndex extends EventEmitter {
indexManager.addEventToIndex(e, profile);
}
/**
* Emmit that the crawler has changed the checkpoint that it's currently
* handling.
*/
emitNewCheckpoint() {
this.emit("changedCheckpoint", this.currentRoom());
}
/**
* The main crawler loop.
*
* Goes through crawlerCheckpoints and fetches events from the server to be
* added to the EventIndex.
*
* If a /room/{roomId}/messages request doesn't contain any events, stop the
* crawl, otherwise create a new checkpoint and push it to the
* crawlerCheckpoints queue so we go through them in a round-robin way.
*/
async crawlerFunc() {
let cancelled = false;
@ -328,8 +406,6 @@ export default class EventIndex extends EventEmitter {
].indexOf(value.getType()) >= 0
&& !value.isRedacted() && !value.isDecryptionFailure()
);
// TODO do we need to check if the event has all the valid
// attributes?
};
// TODO if there are no events at this point we're missing a lot
@ -394,40 +470,28 @@ export default class EventIndex extends EventEmitter {
console.log("EventIndex: Stopping crawler function");
}
onTimelineReset = async (room, timelineSet, resetAllTimelines) => {
if (room === null) return;
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
const backwardsCheckpoint = {
roomId: room.roomId,
token: token,
fullCrawl: false,
direction: "b",
};
console.log("EventIndex: Added checkpoint because of a limited timeline",
backwardsCheckpoint);
await indexManager.addCrawlerCheckpoint(backwardsCheckpoint);
this.crawlerCheckpoints.push(backwardsCheckpoint);
}
/**
* Start the crawler background task.
*/
startCrawler() {
if (this._crawler !== null) return;
this.crawlerFunc();
}
/**
* Stop the crawler background task.
*/
stopCrawler() {
if (this._crawler === null) return;
this._crawler.cancel();
}
/**
* Close the event index.
*
* This removes all the MatrixClient event listeners, stops the crawler
* task, and closes the index.
*/
async close() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
this.removeListeners();
@ -435,6 +499,15 @@ export default class EventIndex extends EventEmitter {
return indexManager.closeEventIndex();
}
/**
* Search the event index using the given term for matching events.
*
* @param {SearchArgs} searchArgs The search configuration for the search,
* sets the search term and determines the search result contents.
*
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
* of search results once the search is done.
*/
async search(searchArgs) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs);
@ -634,6 +707,12 @@ export default class EventIndex extends EventEmitter {
return paginationPromise;
}
/**
* Get statistical information of the index.
*
* @return {Promise<IndexStats>} A promise that will resolve to the index
* statistics.
*/
async getStats() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.getStats();