diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss
index 9ce524c5ac..dc22de4713 100644
--- a/res/css/views/right_panel/_UserInfo.scss
+++ b/res/css/views/right_panel/_UserInfo.scss
@@ -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 {
diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index 29d8207d0a..0f30d9cf61 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -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 = ;
title = _t("Session verified");
+ let message;
+ if (this.state.backupInfo) {
+ message =
{_t(
+ "Your new session is now verified. It has access to your " +
+ "encrypted messages, and other users will see it as trusted.",
+ )}
;
+ } else {
+ message = {_t(
+ "Your new session is now verified. Other users will see it as trusted.",
+ )}
;
+ }
body = (
-
{_t(
- "Your new session is now verified. It has access to your " +
- "encrypted messages, and other users will see it as trusted.",
- )}
+ {message}
);
+ } else if (phase === PHASE_BUSY) {
+ const Spinner = sdk.getComponent('views.elements.Spinner');
+ icon =
;
+ title = '';
+ body =
;
} else {
throw new Error(`Unknown phase ${phase}`);
}
diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js
index de11dbf9fa..f05f0477ee 100644
--- a/src/components/views/dialogs/InviteDialog.js
+++ b/src/components/views/dialogs/InviteDialog.js
@@ -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 (
diff --git a/src/components/views/right_panel/EncryptionPanel.js b/src/components/views/right_panel/EncryptionPanel.js
index d45280e29c..4e147bd7a5 100644
--- a/src/components/views/right_panel/EncryptionPanel.js
+++ b/src/components/views/right_panel/EncryptionPanel.js
@@ -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
;
} else {
diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js
index 3740c6e49d..18a9024310 100644
--- a/src/components/views/right_panel/VerificationPanel.js
+++ b/src/components/views/right_panel/VerificationPanel.js
@@ -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;
}
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 55855bbc9a..7c77c546b6 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -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);
diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js
index 7b93f42983..aee0f57cf8 100644
--- a/src/components/views/verification/VerificationShowSas.js
+++ b/src/components/views/verification/VerificationShowSas.js
@@ -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 =
{
+ async initEventIndex(): Promise {
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 {
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 {
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 {
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 {
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 {
throw new Error("Unimplemented");
}
}
diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js
index 9628920cd7..e1ec0d1d1c 100644
--- a/src/indexing/EventIndex.js
+++ b/src/indexing/EventIndex.js
@@ -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} A promise that will resolve to the index
+ * statistics.
+ */
async getStats() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.getStats();