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; display: block;
margin: 16px 0; 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 { .mx_UserInfo.mx_UserInfo_smallAvatar {

View file

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

View file

@ -351,9 +351,20 @@ export default class InviteDialog extends React.PureComponent {
continue; continue;
} }
const lastEventTs = room.timeline && room.timeline.length // Find the last timestamp for a message event
? room.timeline[room.timeline.length - 1].getTs() const searchTypes = ["m.room.message", "m.room.encrypted", "m.sticker"];
: 0; 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) { if (!lastEventTs) {
// something weird is going on with this room // something weird is going on with this room
console.warn(`[Invite:Recents] ${userId} (${room.roomId}) has a weird last timestamp: ${lastEventTs}`); 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) => { _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 // Prevent the text being pasted into the textarea
e.preventDefault(); e.preventDefault();
@ -937,6 +954,7 @@ export default class InviteDialog extends React.PureComponent {
value={this.state.filterText} value={this.state.filterText}
ref={this._editorRef} ref={this._editorRef}
onPaste={this._onPaste} onPaste={this._onPaste}
autoFocus={true}
/> />
); );
return ( return (

View file

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

View file

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

View file

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

View file

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

View file

@ -51,6 +51,9 @@ export default class EventIndex extends EventEmitter {
this.registerListeners(); this.registerListeners();
} }
/**
* Register event listeners that are necessary for the event index to work.
*/
registerListeners() { registerListeners() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
@ -60,6 +63,9 @@ export default class EventIndex extends EventEmitter {
client.on('Room.timelineReset', this.onTimelineReset); client.on('Room.timelineReset', this.onTimelineReset);
} }
/**
* Remove the event index specific event listeners.
*/
removeListeners() { removeListeners() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (client === null) return; 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) => { onSync = async (state, prevState, data) => {
const indexManager = PlatformPeg.get().getEventIndexingManager(); 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) => { onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
// We only index encrypted rooms locally. // We only index encrypted rooms locally.
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; 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) => { onEventDecrypted = async (ev, err) => {
const eventId = ev.getId(); const eventId = ev.getId();
@ -171,6 +200,41 @@ export default class EventIndex extends EventEmitter {
await this.addLiveEventToIndex(ev); 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) { async addLiveEventToIndex(ev) {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
@ -190,10 +254,24 @@ export default class EventIndex extends EventEmitter {
indexManager.addEventToIndex(e, profile); indexManager.addEventToIndex(e, profile);
} }
/**
* Emmit that the crawler has changed the checkpoint that it's currently
* handling.
*/
emitNewCheckpoint() { emitNewCheckpoint() {
this.emit("changedCheckpoint", this.currentRoom()); 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() { async crawlerFunc() {
let cancelled = false; let cancelled = false;
@ -328,8 +406,6 @@ export default class EventIndex extends EventEmitter {
].indexOf(value.getType()) >= 0 ].indexOf(value.getType()) >= 0
&& !value.isRedacted() && !value.isDecryptionFailure() && !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 // 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"); console.log("EventIndex: Stopping crawler function");
} }
onTimelineReset = async (room, timelineSet, resetAllTimelines) => { /**
if (room === null) return; * Start the crawler background task.
*/
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);
}
startCrawler() { startCrawler() {
if (this._crawler !== null) return; if (this._crawler !== null) return;
this.crawlerFunc(); this.crawlerFunc();
} }
/**
* Stop the crawler background task.
*/
stopCrawler() { stopCrawler() {
if (this._crawler === null) return; if (this._crawler === null) return;
this._crawler.cancel(); this._crawler.cancel();
} }
/**
* Close the event index.
*
* This removes all the MatrixClient event listeners, stops the crawler
* task, and closes the index.
*/
async close() { async close() {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
this.removeListeners(); this.removeListeners();
@ -435,6 +499,15 @@ export default class EventIndex extends EventEmitter {
return indexManager.closeEventIndex(); 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) { async search(searchArgs) {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs); return indexManager.searchEventIndex(searchArgs);
@ -634,6 +707,12 @@ export default class EventIndex extends EventEmitter {
return paginationPromise; return paginationPromise;
} }
/**
* Get statistical information of the index.
*
* @return {Promise<IndexStats>} A promise that will resolve to the index
* statistics.
*/
async getStats() { async getStats() {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.getStats(); return indexManager.getStats();