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();