Merge branch 'experimental' of github.com:matrix-org/matrix-react-sdk into erikj/state_counters

This commit is contained in:
Erik Johnston 2019-01-10 16:56:47 +00:00
commit cbf9ff6aee
58 changed files with 1168 additions and 320 deletions

View file

@ -48,7 +48,7 @@ export default class GroupInviteTileContextMenu extends React.Component {
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
title: _t('Reject invitation'),
description: _t('Are you sure you want to reject the invitation?'),
onFinished: async(shouldLeave) => {
onFinished: async (shouldLeave) => {
if (!shouldLeave) return;
// FIXME: controller shouldn't be loading a view :(

View file

@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component {
};
}
_onClearClick = async(e) => {
_onClearClick = async (e) => {
await MatrixClientPeg.get()._unstable_setStatusMessage("");
this.setState({message: ""});
};

View file

@ -21,6 +21,7 @@ import dis from '../../../dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
export default class TagTileContextMenu extends React.Component {
static propTypes = {
@ -34,6 +35,7 @@ export default class TagTileContextMenu extends React.Component {
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
this._onRemoveClick = this._onRemoveClick.bind(this);
this._onViewAsGridClick = this._onViewAsGridClick.bind(this);
}
_onViewCommunityClick() {
@ -53,8 +55,28 @@ export default class TagTileContextMenu extends React.Component {
this.props.onFinished();
}
_onViewAsGridClick() {
dis.dispatch({
action: 'group_grid_view',
group_id: this.props.tag,
});
this.props.onFinished();
}
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let gridViewOption;
if (SettingsStore.isFeatureEnabled("feature_gridview")) {
gridViewOption = (<div className="mx_TagTileContextMenu_item" onClick={this._onViewAsGridClick} >
<TintableSvg
className="mx_TagTileContextMenu_item_icon"
src="img/feather-icons/grid.svg"
width="15"
height="15"
/>
{ _t('View as Grid') }
</div>);
}
return <div>
<div className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick} >
<TintableSvg
@ -65,6 +87,7 @@ export default class TagTileContextMenu extends React.Component {
/>
{ _t('View Community') }
</div>
{ gridViewOption }
<hr className="mx_TagTileContextMenu_separator" />
<div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} >
<img className="mx_TagTileContextMenu_item_icon" src="img/icon_context_delete.svg" width="15" height="15" />

View file

@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,48 +15,150 @@ limitations under the License.
*/
import React from 'react';
import QuestionDialog from './QuestionDialog';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import SettingsStore from "../../../settings/SettingsStore";
export default (props) => {
const description = _t("For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " +
"please export your room keys for safe-keeping.");
export default class LogoutDialog extends React.Component {
constructor() {
super();
this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this);
this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this);
this._onFinished = this._onFinished.bind(this);
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
}
const onExportE2eKeysClicked = () => {
_onSettingsLinkClick() {
// close dialog
if (this.props.onFinished) {
this.props.onFinished();
}
}
_onExportE2eKeysClicked() {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
);
};
}
const onFinished = (confirmed) => {
_onFinished(confirmed) {
if (confirmed) {
dis.dispatch({action: 'logout'});
}
// close dialog
if (props.onFinished) {
props.onFinished();
if (this.props.onFinished) {
this.props.onFinished();
}
};
}
return (<QuestionDialog
hasCancelButton={true}
title={_t("Sign out")}
description={<div>{description}</div>}
button={_t("Sign out")}
extraButtons={[
(<button key="export" className="mx_Dialog_primary"
onClick={onExportE2eKeysClicked}>
{ _t("Export E2E room keys") }
</button>),
]}
onFinished={onFinished}
/>);
};
_onSetRecoveryMethodClick() {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
);
// close dialog
if (this.props.onFinished) {
this.props.onFinished();
}
}
_onLogoutConfirm() {
dis.dispatch({action: 'logout'});
// close dialog
if (this.props.onFinished) {
this.props.onFinished();
}
}
render() {
let description;
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
description = <div>
<p>{_t(
"When you log out, you'll lose your secure message history. To prevent " +
"this, set up a recovery method.",
)}</p>
<p>{_t(
"Alternatively, advanced users can also manually export encryption keys in " +
"<a>Settings</a> before logging out.", {},
{
a: sub => <a href='#/settings' onClick={this._onSettingsLinkClick}>{sub}</a>,
},
)}</p>
</div>;
} else {
description = <div>{_t(
"For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " +
"please export your room keys for safe-keeping.",
)}</div>;
}
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
// Not quite a standard question dialog as the primary button cancels
// the action and does something else instead, whilst non-default button
// confirms the action.
return (<BaseDialog
title={_t("Warning!")}
contentId='mx_Dialog_content'
hasCancel={false}
onFinsihed={this._onFinished}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ description }
</div>
<DialogButtons primaryButton={_t('Set a Recovery Method')}
hasCancel={false}
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
focus={true}
>
<button onClick={this._onLogoutConfirm}>
{_t("I understand, log out without")}
</button>
</DialogButtons>
</BaseDialog>);
} else {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (<QuestionDialog
hasCancelButton={true}
title={_t("Sign out")}
// TODO: This is made up by me and would need to also mention verifying
// once you can restorew a backup by verifying a device
description={_t(
"When signing in again, you can access encrypted chat history by " +
"restoring your key backup. You'll need your recovery key.",
)}
button={_t("Sign out")}
onFinished={this._onFinished}
/>);
}
} else {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (<QuestionDialog
hasCancelButton={true}
title={_t("Sign out")}
description={description}
button={_t("Sign out")}
extraButtons={[
(<button key="export" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
{ _t("Export E2E room keys") }
</button>),
]}
onFinished={this._onFinished}
/>);
}
}
}

View file

@ -271,7 +271,7 @@ const Pill = React.createClass({
break;
}
const classes = classNames(pillClass, {
const classes = classNames("mx_Pill", pillClass, {
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
"mx_UserPill_selected": this.props.isSelected,
});

View file

@ -78,7 +78,6 @@ export default class HeaderButtons extends React.Component {
// till show_right_panel, just without the fromHeader flag
// as that would hide the right panel again
dis.dispatch(Object.assign({}, payload, {fromHeader: false}));
}
this.setState({
phase: payload.phase,

View file

@ -39,7 +39,6 @@ import Unread from '../../../Unread';
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import RoomViewStore from '../../../stores/RoomViewStore';
import SdkConfig from '../../../SdkConfig';
import MultiInviter from "../../../utils/MultiInviter";
import SettingsStore from "../../../settings/SettingsStore";
@ -50,6 +49,7 @@ module.exports = withMatrixClient(React.createClass({
propTypes: {
matrixClient: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
roomId: PropTypes.string,
},
getInitialState: function() {
@ -713,8 +713,8 @@ module.exports = withMatrixClient(React.createClass({
}
if (!member || !member.membership || member.membership === 'leave') {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
const onInviteUserButton = async() => {
const roomId = member && member.roomId ? member.roomId : this.props.roomId;
const onInviteUserButton = async () => {
try {
// We use a MultiInviter to re-use the invite logic, even though
// we're only inviting one user.

View file

@ -22,7 +22,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../matrix-to';
@ -63,7 +62,7 @@ export default class MessageComposer extends React.Component {
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
},
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
isQuoting: Boolean(this.props.roomViewStore.getQuotingEvent()),
tombstone: this._getRoomTombstone(),
};
}
@ -75,7 +74,7 @@ export default class MessageComposer extends React.Component {
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
this._waitForOwnMember();
}
@ -124,14 +123,14 @@ export default class MessageComposer extends React.Component {
}
_onRoomViewStoreUpdate() {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
const isQuoting = Boolean(this.props.roomViewStore.getQuotingEvent());
if (this.state.isQuoting === isQuoting) return;
this.setState({ isQuoting });
}
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'});
return;
}
@ -165,7 +164,7 @@ export default class MessageComposer extends React.Component {
}
}
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
const isQuoting = Boolean(this.props.roomViewStore.getQuotingEvent());
let replyToWarning = null;
if (isQuoting) {
replyToWarning = <p>{
@ -229,7 +228,7 @@ export default class MessageComposer extends React.Component {
if (!call) {
return;
}
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'hangup',
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
@ -238,7 +237,7 @@ export default class MessageComposer extends React.Component {
}
onCallClick(ev) {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId,
@ -246,7 +245,7 @@ export default class MessageComposer extends React.Component {
}
onVoiceCallClick(ev) {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'place_call',
type: "voice",
room_id: this.props.room.roomId,
@ -282,7 +281,7 @@ export default class MessageComposer extends React.Component {
ev.preventDefault();
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'view_room',
highlighted: true,
room_id: replacementRoomId,
@ -421,8 +420,10 @@ export default class MessageComposer extends React.Component {
controls.push(
<MessageComposerInput
roomViewStore={this.props.roomViewStore}
ref={(c) => this.messageComposerInput = c}
key="controls_input"
isGrid={this.props.isGrid}
onResize={this.props.onResize}
room={this.props.room}
placeholder={placeholderText}
@ -529,5 +530,6 @@ MessageComposer.propTypes = {
uploadAllowed: PropTypes.func.isRequired,
// string representing the current room app drawer state
showApps: PropTypes.bool
showApps: PropTypes.bool,
roomViewStore: PropTypes.object.isRequired,
};

View file

@ -41,8 +41,6 @@ import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import Analytics from '../../../Analytics';
import dis from '../../../dispatcher';
import * as RichText from '../../../RichText';
import * as HtmlUtils from '../../../HtmlUtils';
import Autocomplete from './Autocomplete';
@ -58,7 +56,6 @@ import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList,
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {makeUserPermalink} from "../../../matrix-to";
import ReplyPreview from "./ReplyPreview";
import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread";
import {ContentHelpers} from 'matrix-js-sdk';
@ -121,7 +118,7 @@ function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'message_send_failed',
});
}
@ -135,6 +132,18 @@ function rangeEquals(a: Range, b: Range): boolean {
&& a.isBackward === b.isBackward);
}
class NoopHistoryManager {
getItem() {}
save() {}
get currentIndex() { return 0; }
set currentIndex(_) {}
get history() { return []; }
set history(_) {}
}
/*
* The textInput part of the MessageComposer
*/
@ -150,6 +159,7 @@ export default class MessageComposerInput extends React.Component {
onFilesPasted: PropTypes.func,
onInputStateChanged: PropTypes.func,
roomViewStore: PropTypes.object.isRequired,
};
client: MatrixClient;
@ -344,12 +354,16 @@ export default class MessageComposerInput extends React.Component {
}
componentWillMount() {
this.dispatcherRef = dis.register(this.onAction);
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
this.dispatcherRef = this.props.roomViewStore.getDispatcher().register(this.onAction);
if (this.props.isGrid) {
this.historyManager = new NoopHistoryManager();
} else {
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
}
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
this.props.roomViewStore.getDispatcher().unregister(this.dispatcherRef);
}
_collectEditor = (e) => {
@ -1120,7 +1134,7 @@ export default class MessageComposerInput extends React.Component {
return true;
}
const replyingToEv = RoomViewStore.getQuotingEvent();
const replyingToEv = this.props.roomViewStore.getQuotingEvent();
const mustSendHTML = Boolean(replyingToEv);
if (this.state.isRichTextEnabled) {
@ -1208,14 +1222,14 @@ export default class MessageComposerInput extends React.Component {
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'reply_to_event',
event: null,
});
}
this.client.sendMessage(this.props.room.roomId, content).then((res) => {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'message_sent',
});
}).catch((e) => {
@ -1260,7 +1274,7 @@ export default class MessageComposerInput extends React.Component {
}
};
selectHistory = async(up) => {
selectHistory = async (up) => {
const delta = up ? -1 : 1;
// True if we are not currently selecting history, but composing a message
@ -1308,7 +1322,7 @@ export default class MessageComposerInput extends React.Component {
return true;
};
onTab = async(e) => {
onTab = async (e) => {
this.setState({
someCompletions: null,
});
@ -1330,7 +1344,7 @@ export default class MessageComposerInput extends React.Component {
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
};
onEscape = async(e) => {
onEscape = async (e) => {
e.preventDefault();
if (this.autocomplete) {
this.autocomplete.onEscape(e);
@ -1349,7 +1363,7 @@ export default class MessageComposerInput extends React.Component {
/* If passed null, restores the original editor content from state.originalEditorState.
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
*/
setDisplayedCompletion = async(displayedCompletion: ?Completion): boolean => {
setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => {
const activeEditorState = this.state.originalEditorState || this.state.editorState;
if (displayedCompletion == null) {
@ -1589,7 +1603,7 @@ export default class MessageComposerInput extends React.Component {
return (
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
<div className="mx_MessageComposer_autocomplete_wrapper">
<ReplyPreview />
<ReplyPreview roomViewStore={this.props.roomViewStore} />
<Autocomplete
ref={(e) => this.autocomplete = e}
room={this.props.room}

View file

@ -18,7 +18,6 @@ import React from 'react';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
function cancelQuoting() {
@ -38,7 +37,7 @@ export default class ReplyPreview extends React.Component {
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate();
}
@ -50,7 +49,7 @@ export default class ReplyPreview extends React.Component {
}
_onRoomViewStoreUpdate() {
const event = RoomViewStore.getQuotingEvent();
const event = this.props.roomViewStore.getQuotingEvent();
if (this.state.event !== event) {
this.setState({ event });
}

View file

@ -24,6 +24,7 @@ import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from "../../../Modal";
import RateLimitedFunc from '../../../ratelimitedfunc';
import dis from '../../../dispatcher';
import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
@ -152,6 +153,14 @@ module.exports = React.createClass({
});
},
onToggleRightPanelClick: function(ev) {
if (this.props.collapsedRhs) {
dis.dispatch({action: "show_right_panel"});
} else {
dis.dispatch({action: "hide_right_panel"});
}
},
_hasUnreadPins: function() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
@ -409,6 +418,17 @@ module.exports = React.createClass({
</div>;
}
let toggleRightPanelButton;
if (this.props.isGrid) {
toggleRightPanelButton =
<AccessibleButton
className="mx_RoomHeader_button"
onClick={this.onToggleRightPanelClick}
title={_t('Toggle right panel')}>
<TintableSvg src="img/feather-icons/toggle-right-panel.svg" width="20" height="20" />
</AccessibleButton>;
}
return (
<div className={"mx_RoomHeader light-panel " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
<div className="mx_RoomHeader_wrapper">
@ -419,7 +439,8 @@ module.exports = React.createClass({
{ saveButton }
{ cancelButton }
{ rightRow }
<RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} />
{ !this.props.isGrid ? <RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} /> : undefined }
{ toggleRightPanelButton }
</div>
</div>
);

View file

@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -20,10 +20,16 @@ import sdk from "../../../index";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import MatrixClientPeg from "../../../MatrixClientPeg";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class RoomRecoveryReminder extends React.PureComponent {
static propTypes = {
onFinished: PropTypes.func.isRequired,
// called if the user sets the option to suppress this reminder in the future
onDontAskAgainSet: PropTypes.func,
}
static defaultProps = {
onDontAskAgainSet: function() {},
}
constructor(props) {
@ -82,7 +88,6 @@ export default class RoomRecoveryReminder extends React.PureComponent {
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().credentials.userId,
device: this.state.unverifiedDevice,
onFinished: this.props.onFinished,
});
return;
}
@ -91,9 +96,6 @@ export default class RoomRecoveryReminder extends React.PureComponent {
// we'll show the create key backup flow.
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
{
onFinished: this.props.onFinished,
},
);
}
@ -103,10 +105,14 @@ export default class RoomRecoveryReminder extends React.PureComponent {
Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder",
import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"),
{
onDontAskAgain: () => {
// Report false to the caller, who should prevent the
// reminder from appearing in the future.
this.props.onFinished(false);
onDontAskAgain: async () => {
await SettingsStore.setValue(
"showRoomRecoveryReminder",
null,
SettingLevel.ACCOUNT,
false,
);
this.props.onDontAskAgainSet();
},
onSetup: () => {
this.showSetupDialog();

View file

@ -29,7 +29,6 @@ import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import AccessibleButton from '../elements/AccessibleButton';
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
@ -62,7 +61,7 @@ module.exports = React.createClass({
roomName: this.props.room.name,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
notificationCount: this.props.room.getUnreadNotificationCount(),
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
selected: this.props.room.roomId === ActiveRoomObserver.getActiveRoomId(),
});
},
@ -117,9 +116,9 @@ module.exports = React.createClass({
}
},
_onActiveRoomChange: function() {
_onActiveRoomChange: function(activeRoomId) {
this.setState({
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
selected: this.props.room.roomId === activeRoomId,
});
},

View file

@ -21,13 +21,15 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
export default class KeyBackupPanel extends React.Component {
export default class KeyBackupPanel extends React.PureComponent {
constructor(props) {
super(props);
this._startNewBackup = this._startNewBackup.bind(this);
this._deleteBackup = this._deleteBackup.bind(this);
this._verifyDevice = this._verifyDevice.bind(this);
this._onKeyBackupSessionsRemaining =
this._onKeyBackupSessionsRemaining.bind(this);
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
this._restoreBackup = this._restoreBackup.bind(this);
@ -36,6 +38,7 @@ export default class KeyBackupPanel extends React.Component {
loading: true,
error: null,
backupInfo: null,
sessionsRemaining: 0,
};
}
@ -43,6 +46,10 @@ export default class KeyBackupPanel extends React.Component {
this._loadBackupStatus();
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
MatrixClientPeg.get().on(
'crypto.keyBackupSessionsRemaining',
this._onKeyBackupSessionsRemaining,
);
}
componentWillUnmount() {
@ -50,9 +57,19 @@ export default class KeyBackupPanel extends React.Component {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
MatrixClientPeg.get().removeListener(
'crypto.keyBackupSessionsRemaining',
this._onKeyBackupSessionsRemaining,
);
}
}
_onKeyBackupSessionsRemaining(sessionsRemaining) {
this.setState({
sessionsRemaining,
});
}
_onKeyBackupStatus() {
this._loadBackupStatus();
}
@ -144,57 +161,70 @@ export default class KeyBackupPanel extends React.Component {
} else if (this.state.backupInfo) {
let clientBackupStatus;
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = _t("This device is uploading keys to this backup");
clientBackupStatus = _t("This device is using key backup");
} else {
// XXX: display why and how to fix it
clientBackupStatus = _t(
"This device is <b>not</b> uploading keys to this backup", {},
"This device is <b>not</b> using key backup", {},
{b: x => <b>{x}</b>},
);
}
let uploadStatus;
const { sessionsRemaining } = this.state;
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
// No upload status to show when backup disabled.
uploadStatus = "";
} else if (sessionsRemaining > 0) {
uploadStatus = <div>
{_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })} <br />
</div>;
} else {
uploadStatus = <div>
{_t("All keys backed up")} <br />
</div>;
}
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
const deviceName = sig.device.getDisplayName() || sig.device.deviceId;
const sigStatusSubstitutions = {
validity: sub =>
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
{sub}
</span>,
verify: sub =>
<span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
{sub}
</span>,
device: sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>,
};
const validity = sub =>
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
{sub}
</span>;
const verify = sub =>
<span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
{sub}
</span>;
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
let sigStatus;
if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from this device",
{}, sigStatusSubstitutions,
{}, { validity },
);
} else if (sig.valid && sig.device.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>verified</verify> device <device></device>",
{}, sigStatusSubstitutions,
{}, { validity, verify, device },
);
} else if (sig.valid && !sig.device.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>unverified</verify> device <device></device>",
{}, sigStatusSubstitutions,
{}, { validity, verify, device },
);
} else if (!sig.valid && sig.device.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>verified</verify> device <device></device>",
{}, sigStatusSubstitutions,
{}, { validity, verify, device },
);
} else if (!sig.valid && !sig.device.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>unverified</verify> device <device></device>",
{}, sigStatusSubstitutions,
{}, { validity, verify, device },
);
}
@ -219,6 +249,7 @@ export default class KeyBackupPanel extends React.Component {
{_t("Backup version: ")}{this.state.backupInfo.version}<br />
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}<br />
{clientBackupStatus}<br />
{uploadStatus}
<div>{backupSigStatuses}</div><br />
<br />
<AccessibleButton className="mx_UserSettings_button"