Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Weblate 2019-02-13 09:40:29 +00:00
commit e3f4891489
43 changed files with 701 additions and 356 deletions

View file

@ -57,6 +57,7 @@
@import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_EncryptedEventDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_RestoreKeyBackupDialog.scss"; @import "./views/dialogs/_RestoreKeyBackupDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss";
@ -126,6 +127,7 @@
@import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PinnedEventsPanel.scss";
@import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_PresenceLabel.scss";
@import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_ReplyPreview.scss";
@import "./views/rooms/_RoomBreadcrumbs.scss";
@import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomDropTarget.scss";
@import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomList.scss";

View file

@ -28,8 +28,8 @@ limitations under the License.
} }
.mx_TabbedView_tabLabels { .mx_TabbedView_tabLabels {
width: 150px; width: 170px;
max-width: 150px; max-width: 170px;
color: $tab-label-fg-color; color: $tab-label-fg-color;
position: fixed; position: fixed;
} }
@ -39,9 +39,8 @@ limitations under the License.
cursor: pointer; cursor: pointer;
display: block; display: block;
border-radius: 3px; border-radius: 3px;
font-size: 12px; font-size: 14px;
font-weight: 600; min-height: 24px; // use min-height instead of height to allow the label to overflow a bit
min-height: 20px; // use min-height instead of height to allow the label to overflow a bit
margin-bottom: 6px; margin-bottom: 6px;
position: relative; position: relative;
} }
@ -55,8 +54,8 @@ limitations under the License.
margin-left: 6px; margin-left: 6px;
margin-right: 9px; margin-right: 9px;
margin-top: 1px; margin-top: 1px;
width: 14px; width: 16px;
height: 14px; height: 16px;
display: inline-block; display: inline-block;
} }
@ -64,9 +63,9 @@ limitations under the License.
display: inline-block; display: inline-block;
background-color: $tab-label-icon-bg-color; background-color: $tab-label-icon-bg-color;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: 14px; mask-size: 16px;
width: 14px; width: 14px;
height: 18px; height: 22px;
mask-position: center; mask-position: center;
content: ''; content: '';
vertical-align: middle; vertical-align: middle;
@ -81,7 +80,7 @@ limitations under the License.
} }
.mx_TabbedView_tabPanel { .mx_TabbedView_tabPanel {
margin-left: 220px; // 150px sidebar + 70px padding margin-left: 240px; // 170px sidebar + 70px padding
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -0,0 +1,24 @@
/*
Copyright 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_IncomingSasDialog_opponentProfile_image {
position: relative;
}
.mx_IncomingSasDialog_opponentProfile h2 {
display: inline-block;
margin-left: 10px;
}

View file

@ -16,8 +16,8 @@ limitations under the License.
.mx_SettingsDialog { .mx_SettingsDialog {
.mx_Dialog { .mx_Dialog {
max-width: 900px; max-width: 1000px;
width: 80%; width: 90%;
height: 80%; height: 80%;
border-radius: 4px; border-radius: 4px;
padding-top: 0; padding-top: 0;
@ -30,7 +30,7 @@ limitations under the License.
.mx_TabbedView .mx_SettingsTab { .mx_TabbedView .mx_SettingsTab {
box-sizing: border-box; box-sizing: border-box;
min-width: 550px; min-width: 580px;
padding-right: 130px; padding-right: 130px;
// Put some padding on the bottom to avoid the settings tab from // Put some padding on the bottom to avoid the settings tab from

View file

@ -28,28 +28,28 @@ limitations under the License.
flex-direction: column; flex-direction: column;
} }
.mx_UnknownDeviceDialog ul {
list-style: none;
padding: 0;
}
// userid
.mx_UnknownDeviceDialog p {
font-weight: bold;
font-size: 16px;
}
.mx_UnknownDeviceDialog .mx_DeviceVerifyButtons { .mx_UnknownDeviceDialog .mx_DeviceVerifyButtons {
float: right; flex-direction: row !important;
} }
.mx_UnknownDeviceDialog .mx_Dialog_content { .mx_UnknownDeviceDialog .mx_Dialog_content {
margin-bottom: 24px; margin-bottom: 24px;
} }
.mx_UnknownDeviceDialog .mx_MemberDeviceInfo { .mx_UnknownDeviceDialog_deviceList > li {
float: right; padding: 4px;
clear: both;
padding: 0px;
padding-top: 8px;
} }
.mx_UnknownDeviceDialog .mx_MemberDeviceInfo_textButton { .mx_UnknownDeviceDialog_deviceList > li > * {
@mixin mx_DialogButton_small; padding-bottom: 0;
background-color: $primary-bg-color;
color: $accent-color;
}
.mx_UnknownDeviceDialog .mx_UnknownDeviceDialog_deviceList li {
height: 40px;
border-bottom: 1px solid $primary-hairline-color;
} }

View file

@ -14,6 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/*
the tile title bar is 5 (top border) + 12 (title, buttons) + 5 (bottom padding) px = 22px
the body is assumed to be 300px (assumed by at least the sticker pickerm, perhaps elsewhere),
so the body height would be 300px - 22px (room for title bar) = 278px
BUT! the sticker picker also assumes it's a little less high than that because the iframe
for the sticker picker doesn't have any padding or margin on it's bottom.
so subtracking another 5px, which brings us at 273px.
*/
$AppsDrawerBodyHeight: 273px;
.mx_AppsDrawer { .mx_AppsDrawer {
margin: 5px; margin: 5px;
} }
@ -83,7 +93,7 @@ limitations under the License.
} }
.mx_AppTile_persistedWrapper { .mx_AppTile_persistedWrapper {
height: 280px; height: $AppsDrawerBodyHeight;
} }
.mx_AppTile_mini .mx_AppTile_persistedWrapper { .mx_AppTile_mini .mx_AppTile_persistedWrapper {
@ -189,7 +199,7 @@ limitations under the License.
} }
.mx_AppTileBody{ .mx_AppTileBody{
height: 280px; height: $AppsDrawerBodyHeight;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
} }
@ -208,7 +218,7 @@ limitations under the License.
.mx_AppTileBody iframe { .mx_AppTileBody iframe {
width: 100%; width: 100%;
height: 280px; height: $AppsDrawerBodyHeight;
overflow: hidden; overflow: hidden;
border: none; border: none;
padding: 0; padding: 0;
@ -332,7 +342,7 @@ form.mx_Custom_Widget_Form div {
align-items: center; align-items: center;
font-weight: bold; font-weight: bold;
position: relative; position: relative;
height: 280px; height: $AppsDrawerBodyHeight;
} }
.mx_AppLoading .mx_Spinner { .mx_AppLoading .mx_Spinner {

View file

@ -533,7 +533,7 @@ limitations under the License.
} }
.mx_EventTile_e2eIcon { .mx_EventTile_e2eIcon {
top: 7px; top: 3px;
} }
.mx_EventTile_editButton { .mx_EventTile_editButton {

View file

@ -0,0 +1,54 @@
/*
Copyright 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomBreadcrumbs {
overflow-x: auto;
position: relative;
height: 32px;
margin: 8px;
margin-bottom: 0;
overflow-x: hidden;
display: flex;
flex-direction: row;
> * {
margin-left: 4px;
}
&::after {
content: "";
position: absolute;
width: 15px;
top: 0;
right: 0;
height: 100%;
background: linear-gradient(to right, rgba(242,245,248,0), rgba(242,245,248,1));
}
.mx_RoomBreadcrumbs_animate {
margin-left: 0;
transition: transform 0.3s, width 0.3s;
width: 32px;
transform: scale(1);
}
.mx_RoomBreadcrumbs_preAnimate {
width: 0;
transform: scale(0);
}
}

View file

@ -7,8 +7,12 @@
height: 300px; height: 300px;
} }
.mx_Stickers_content .mx_AppTileFullWidth { #mx_persistedElement_stickerPicker .mx_AppTileFullWidth {
border: none; height: unset;
box-sizing: border-box;
border-left: none;
border-right: none;
border-bottom: none;
} }
.mx_Stickers_contentPlaceholder { .mx_Stickers_contentPlaceholder {

View file

@ -21,7 +21,7 @@ limitations under the License.
} }
.mx_SettingsTab_subheading { .mx_SettingsTab_subheading {
font-size: 14px; font-size: 16px;
display: block; display: block;
font-family: $font-family; font-family: $font-family;
font-weight: 600; font-weight: 600;
@ -32,7 +32,7 @@ limitations under the License.
.mx_SettingsTab_subsectionText { .mx_SettingsTab_subsectionText {
color: $settings-subsection-fg-color; color: $settings-subsection-fg-color;
font-size: 12px; font-size: 14px;
padding-bottom: 12px; padding-bottom: 12px;
display: block; display: block;
margin: 0 100px 0 0; // Align with the rest of the view margin: 0 100px 0 0; // Align with the rest of the view
@ -40,16 +40,17 @@ limitations under the License.
.mx_SettingsTab_section .mx_SettingsFlag { .mx_SettingsTab_section .mx_SettingsFlag {
margin-right: 100px; margin-right: 100px;
height: 25px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_label { .mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_label {
vertical-align: bottom; vertical-align: middle;
display: inline-block; display: inline-block;
font-size: 12px; font-size: 14px;
color: $primary-fg-color; color: $primary-fg-color;
max-width: calc(100% - 48px); // Force word wrap instead of colliding with the switch max-width: calc(100% - 48px); // Force word wrap instead of colliding with the switch
box-sizing: border-box;
padding-right: 10px;
} }
.mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch { .mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch {

View file

@ -1,5 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12"> <?xml version="1.0" encoding="utf-8"?>
<g fill="none" fill-rule="evenodd" stroke="#454545" stroke-linecap="round" stroke-linejoin="round"> <!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<path d="M6.018 1.532l-4.864 7.81c-.204.34-.205.76-.003 1.1.202.341.577.554.985.558h9.728c.408-.004.783-.217.985-.558.202-.34.201-.76-.003-1.1l-4.864-7.81A1.159 1.159 0 0 0 7 1c-.401 0-.774.202-.982.532zM7 3v4"/> <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 14 12" style="enable-background:new 0 0 14 12;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#454545;stroke-linecap:round;stroke-linejoin:round;}
</style>
<g>
<path class="st0" d="M6,1.5L1.2,9.3c-0.2,0.3-0.2,0.8,0,1.1c0.2,0.3,0.6,0.6,1,0.6h9.7c0.4,0,0.8-0.2,1-0.6c0.2-0.3,0.2-0.8,0-1.1
L8,1.5C7.8,1.2,7.4,1,7,1C6.6,1,6.2,1.2,6,1.5z M7,4v3"/>
</g> </g>
<line class="st0" x1="7" y1="9" x2="7" y2="9"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 704 B

Before After
Before After

View file

@ -32,7 +32,7 @@ module.exports = {
return false; return false;
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
return false; return false;
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { } else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
return false; return false;
} }
const EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');

View file

@ -240,7 +240,6 @@ export default React.createClass({
_renderPhasePassPhrase: function() { _renderPhasePassPhrase: function() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let strengthMeter; let strengthMeter;
let helpText; let helpText;
@ -265,8 +264,15 @@ export default React.createClass({
} }
return <div> return <div>
<p>{_t("Secure your encrypted message history with a Recovery Passphrase.")}</p> <p>{_t(
<p>{_t("You'll need it if you log out or lose access to this device.")}</p> "<b>Warning</b>: you should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"We'll store an encrypted copy of your keys on our server. " +
"Protect your backup with a passphrase to keep it secure.",
)}</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -291,34 +297,12 @@ export default React.createClass({
disabled={!this._passPhraseIsValid()} disabled={!this._passPhraseIsValid()}
/> />
<p>{_t( <details>
"If you don't want encrypted message history to be available on other devices, "+ <summary>{_t("Advanced")}</summary>
"<button>opt out</button>.", <p><button onClick={this._onSkipPassPhraseClick} >
{}, {_t("Set up with a Recovery Key")}
{ </button></p>
button: sub => <AccessibleButton </details>
element="span"
className="mx_linkButton"
onClick={this._onOptOutClick}
>
{sub}
</AccessibleButton>,
},
)}</p>
<p>{_t(
"Or, if you don't want to create a Recovery Passphrase, skip this step and "+
"<button>download a recovery key</button>.",
{},
{
button: sub => <AccessibleButton
element="span"
className="mx_linkButton"
onClick={this._onSkipPassPhraseClick}
>
{sub}
</AccessibleButton>,
},
)}</p>
</div>; </div>;
}, },
@ -353,9 +337,7 @@ export default React.createClass({
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
<p>{_t( <p>{_t(
"Type in your Recovery Passphrase to confirm you remember it. " + "Please enter your passphrase a second time to confirm.",
"If it helps, add it to your password manager or store it " +
"somewhere safe.",
)}</p> )}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -392,7 +374,13 @@ export default React.createClass({
} }
return <div> return <div>
<p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p> <p>{_t(
"Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your passphrase.",
)}</p>
<p>{_t(
"Keep your recovery key somewhere very secure, like a password manager (or a safe)",
)}</p>
<p>{bodyText}</p> <p>{bodyText}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader"> <div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
@ -455,10 +443,9 @@ export default React.createClass({
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
<p>{_t( <p>{_t(
"Your encryption keys are now being backed up in the background " + "Your keys are being backed up (the first backup could take a few minutes).",
"to your Homeserver. The initial backup could take several minutes. " + )}</p>
"You can view key backup upload progress in Settings.")}</p> <DialogButtons primaryButton={_t('Okay')}
<DialogButtons primaryButton={_t('Close')}
onPrimaryButtonClick={this._onDone} onPrimaryButtonClick={this._onDone}
hasCancel={false} hasCancel={false}
/> />
@ -484,19 +471,19 @@ export default React.createClass({
_titleForPhase: function(phase) { _titleForPhase: function(phase) {
switch (phase) { switch (phase) {
case PHASE_PASSPHRASE: case PHASE_PASSPHRASE:
return _t('Create a Recovery Passphrase'); return _t('Secure your backup with a passphrase');
case PHASE_PASSPHRASE_CONFIRM: case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm Recovery Passphrase'); return _t('Confirm your passphrase');
case PHASE_OPTOUT_CONFIRM: case PHASE_OPTOUT_CONFIRM:
return _t('Warning!'); return _t('Warning!');
case PHASE_SHOWKEY: case PHASE_SHOWKEY:
return _t('Recovery Key'); return _t('Recovery key');
case PHASE_KEEPITSAFE: case PHASE_KEEPITSAFE:
return _t('Keep it safe'); return _t('Keep it safe');
case PHASE_BACKINGUP: case PHASE_BACKINGUP:
return _t('Starting backup...'); return _t('Starting backup...');
case PHASE_DONE: case PHASE_DONE:
return _t('Backup Started'); return _t('Success!');
default: default:
return _t("Create Key Backup"); return _t("Create Key Backup");
} }

View file

@ -39,36 +39,8 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
} }
onSetupClick = async () => { onSetupClick = async () => {
// TODO: Should change to a restore key backup flow that checks the const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
// recovery passphrase while at the same time also cross-signing the Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
// device as well in a single flow. Since we don't have that yet, we'll
// look for an unverified device and verify it. Note that this means
// we won't restore keys yet; instead we'll only trust the backup for
// sending our own new keys to it.
let backupSigStatus;
try {
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(this.props.newVersionInfo);
} catch (e) {
console.log("Unable to fetch key backup status", e);
return;
}
let unverifiedDevice;
for (const sig of backupSigStatus.sigs) {
if (!sig.device.isVerified()) {
unverifiedDevice = sig.device;
break;
}
}
if (!unverifiedDevice) {
console.log("Unable to find a device to verify.");
return;
}
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().credentials.userId,
device: unverifiedDevice,
onFinished: this.props.onFinished, onFinished: this.props.onFinished,
}); });
} }
@ -111,11 +83,6 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
} else { } else {
content = <div> content = <div>
{newMethodDetected} {newMethodDetected}
<p>{_t(
"Setting up Secure Messages on this device " +
"will re-encrypt this device's message history with " +
"the new recovery method.",
)}</p>
{hackWarning} {hackWarning}
<DialogButtons <DialogButtons
primaryButton={_t("Set up Secure Messages")} primaryButton={_t("Set up Secure Messages")}

View file

@ -182,6 +182,7 @@ const LeftPanel = React.createClass({
render: function() { render: function() {
const RoomList = sdk.getComponent('rooms.RoomList'); const RoomList = sdk.getComponent('rooms.RoomList');
const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs');
const TagPanel = sdk.getComponent('structures.TagPanel'); const TagPanel = sdk.getComponent('structures.TagPanel');
const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel'); const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel');
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton'); const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
@ -215,12 +216,17 @@ const LeftPanel = React.createClass({
onCleared={ this.onSearchCleared } onCleared={ this.onSearchCleared }
collapsed={this.props.collapsed} />); collapsed={this.props.collapsed} />);
let breadcrumbs;
if (SettingsStore.isFeatureEnabled("feature_room_breadcrumbs")) {
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
return ( return (
<div className={containerClasses}> <div className={containerClasses}>
{ tagPanelContainer } { tagPanelContainer }
<aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }> <aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
<TopLeftMenuButton collapsed={ this.props.collapsed } /> <TopLeftMenuButton collapsed={ this.props.collapsed } />
{ breadcrumbs }
{ searchBox } { searchBox }
<CallPreview ConferenceHandler={VectorConferenceHandler} /> <CallPreview ConferenceHandler={VectorConferenceHandler} />
<RoomList <RoomList

View file

@ -574,11 +574,8 @@ export default React.createClass({
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog'); Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog');
// View the home page if we need something to look at // View the welcome or home page if we need something to look at
if (!this.state.currentGroupId && !this.state.currentRoomId) { this._viewSomethingBehindModal();
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
}
break; break;
} }
case 'view_create_room': case 'view_create_room':
@ -595,11 +592,8 @@ export default React.createClass({
config: this.props.config, config: this.props.config,
}, 'mx_RoomDirectory_dialogWrapper'); }, 'mx_RoomDirectory_dialogWrapper');
// View the home page if we need something to look at // View the welcome or home page if we need something to look at
if (!this.state.currentGroupId && !this.state.currentRoomId) { this._viewSomethingBehindModal();
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
}
} }
break; break;
case 'view_my_groups': case 'view_my_groups':
@ -825,6 +819,7 @@ export default React.createClass({
this.focusComposer = true; this.focusComposer = true;
const newState = { const newState = {
view: VIEWS.LOGGED_IN,
currentRoomId: roomInfo.room_id || null, currentRoomId: roomInfo.room_id || null,
page_type: PageTypes.RoomView, page_type: PageTypes.RoomView,
thirdPartyInvite: roomInfo.third_party_invite, thirdPartyInvite: roomInfo.third_party_invite,
@ -887,6 +882,16 @@ export default React.createClass({
this.notifyNewScreen('group/' + groupId); this.notifyNewScreen('group/' + groupId);
}, },
_viewSomethingBehindModal() {
if (this.state.view !== VIEWS.LOGGED_IN) {
this._viewWelcome();
return;
}
if (!this.state.currentGroupId && !this.state.currentRoomId) {
this._viewHome();
}
},
_viewWelcome() { _viewWelcome() {
this.setStateForNewView({ this.setStateForNewView({
view: VIEWS.WELCOME, view: VIEWS.WELCOME,
@ -1552,11 +1557,7 @@ export default React.createClass({
payload.room_id = roomString; payload.room_id = roomString;
} }
// we can't view a room unless we're logged in
// (a guest account is fine)
if (this.state.view === VIEWS.LOGGED_IN) {
dis.dispatch(payload); dis.dispatch(payload);
}
} else if (screen.indexOf('user/') == 0) { } else if (screen.indexOf('user/') == 0) {
const userId = screen.substring(5); const userId = screen.substring(5);

View file

@ -78,6 +78,11 @@ module.exports = React.createClass({
this.protocols = null; this.protocols = null;
this.setState({protocolsLoading: true}); this.setState({protocolsLoading: true});
if (!MatrixClientPeg.get()) {
// We may not have a client yet when invoked from welcome page
this.setState({protocolsLoading: false});
return;
}
MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
this.protocols = response; this.protocols = response;
this.setState({protocolsLoading: false}); this.setState({protocolsLoading: false});

View file

@ -145,6 +145,7 @@ const RoomSubList = React.createClass({
collapsed={this.props.collapsed || false} collapsed={this.props.collapsed || false}
unread={Unread.doesRoomHaveUnreadMessages(room)} unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite} highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite}
notificationCount={room.getUnreadNotificationCount()}
isInvite={this.props.isInvite} isInvite={this.props.isInvite}
refreshSubList={this._updateSubListCount} refreshSubList={this._updateSubListCount}
incomingCall={null} incomingCall={null}

View file

@ -283,6 +283,15 @@ module.exports = React.createClass({
} }
}, },
_getRoomId() {
// According to `_onRoomViewStoreUpdate`, `state.roomId` can be null
// if we have a room alias we haven't resolved yet. To work around this,
// first we'll try the room object if it's there, and then fallback to
// the bare room ID. (We may want to update `state.roomId` after
// resolving aliases, so we could always trust it.)
return this.state.room ? this.state.room.roomId : this.state.roomId;
},
_onWidgetEchoStoreUpdate: function() { _onWidgetEchoStoreUpdate: function() {
this.setState({ this.setState({
showApps: this._shouldShowApps(this.state.room), showApps: this._shouldShowApps(this.state.room),
@ -784,6 +793,7 @@ module.exports = React.createClass({
this._updateConfCallNotification(); this._updateConfCallNotification();
this._updateDMState(); this._updateDMState();
this._checkIfAlone(this.state.room); this._checkIfAlone(this.state.room);
this._updateE2EStatus(this.state.room);
}, 500), }, 500),
_checkIfAlone: function(room) { _checkIfAlone: function(room) {
@ -877,13 +887,12 @@ module.exports = React.createClass({
// If the user is a ROU, allow them to transition to a PWLU // If the user is a ROU, allow them to transition to a PWLU
if (cli && cli.isGuest()) { if (cli && cli.isGuest()) {
// Join this room once the user has registered and logged in // Join this room once the user has registered and logged in
const signUrl = this.props.thirdPartyInvite ? // (If we failed to peek, we may not have a valid room object.)
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({ dis.dispatch({
action: 'do_after_sync_prepared', action: 'do_after_sync_prepared',
deferred_action: { deferred_action: {
action: 'view_room', action: 'view_room',
room_id: this.state.room.roomId, room_id: this._getRoomId(),
}, },
}); });

View file

@ -21,7 +21,7 @@ import { _t } from '../../languageHandler';
import { KeyCode } from '../../Keyboard'; import { KeyCode } from '../../Keyboard';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import rate_limited_func from '../../ratelimitedfunc'; import { debounce } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton'; import AccessibleButton from '../../components/views/elements/AccessibleButton';
module.exports = React.createClass({ module.exports = React.createClass({
@ -67,12 +67,9 @@ module.exports = React.createClass({
this.onSearch(); this.onSearch();
}, },
onSearch: new rate_limited_func( onSearch: debounce(function() {
function() {
this.props.onSearch(this.refs.search.value); this.props.onSearch(this.refs.search.value);
}, }, 200, {trailing: true}),
500,
),
_onKeyDown: function(ev) { _onKeyDown: function(ev) {
switch (ev.keyCode) { switch (ev.keyCode) {

View file

@ -90,6 +90,11 @@ module.exports = React.createClass({
this.closeMenu(); this.closeMenu();
}, },
e2eInfoClicked: function() {
this.props.e2eInfoCallback();
this.closeMenu();
},
onViewSourceClick: function() { onViewSourceClick: function() {
const ViewSource = sdk.getComponent('structures.ViewSource'); const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, { Modal.createTrackedDialog('View Event Source', '', ViewSource, {
@ -332,6 +337,13 @@ module.exports = React.createClass({
); );
} }
let e2eInfo;
if (this.props.e2eInfoCallback) {
e2eInfo = <div className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
{ _t('End-to-end encryption information') }
</div>;
}
return ( return (
<div className="mx_MessageContextMenu"> <div className="mx_MessageContextMenu">
{ resendButton } { resendButton }
@ -347,6 +359,7 @@ module.exports = React.createClass({
{ replyButton } { replyButton }
{ externalURLButton } { externalURLButton }
{ collapseReplyThread } { collapseReplyThread }
{ e2eInfo }
</div> </div>
); );
}, },

View file

@ -68,7 +68,7 @@ export default class TagTileContextMenu extends React.Component {
<hr className="mx_TagTileContextMenu_separator" /> <hr className="mx_TagTileContextMenu_separator" />
<div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} > <div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} >
<img className="mx_TagTileContextMenu_item_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" /> <img className="mx_TagTileContextMenu_item_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
{ _t('Remove') } { _t('Hide') }
</div> </div>
</div>; </div>;
} }

View file

@ -60,6 +60,11 @@ export default class DeviceVerifyDialog extends React.Component {
} }
_onSwitchToLegacyClick = () => { _onSwitchToLegacyClick = () => {
if (this._verifier) {
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier.cancel('User cancel');
this._verifier = null;
}
this.setState({mode: MODE_LEGACY}); this.setState({mode: MODE_LEGACY});
} }
@ -184,11 +189,21 @@ export default class DeviceVerifyDialog extends React.Component {
_renderSasVerificationPhaseWaitAccept() { _renderSasVerificationPhaseWaitAccept() {
const Spinner = sdk.getComponent("views.elements.Spinner"); const Spinner = sdk.getComponent("views.elements.Spinner");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
return ( return (
<div> <div>
<Spinner /> <Spinner />
<p>{_t("Waiting for partner to accept...")}</p> <p>{_t("Waiting for partner to accept...")}</p>
<p>{_t(
"Nothing appearing? Not all clients support interactive verification yet. " +
"<button>Use legacy verification</button>.",
{}, {button: sub => <AccessibleButton element='span' className="mx_linkButton"
onClick={this._onSwitchToLegacyClick}
>
{sub}
</AccessibleButton>},
)}</p>
</div> </div>
); );
} }

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -37,9 +38,12 @@ export default class IncomingSasDialog extends React.Component {
this.state = { this.state = {
phase: PHASE_START, phase: PHASE_START,
sasVerified: false, sasVerified: false,
opponentProfile: null,
opponentProfileError: null,
}; };
this.props.verifier.on('show_sas', this._onVerifierShowSas); this.props.verifier.on('show_sas', this._onVerifierShowSas);
this.props.verifier.on('cancel', this._onVerifierCancel); this.props.verifier.on('cancel', this._onVerifierCancel);
this._fetchOpponentProfile();
} }
componentWillUnmount() { componentWillUnmount() {
@ -49,6 +53,21 @@ export default class IncomingSasDialog extends React.Component {
this.props.verifier.removeListener('show_sas', this._onVerifierShowSas); this.props.verifier.removeListener('show_sas', this._onVerifierShowSas);
} }
async _fetchOpponentProfile() {
try {
const prof = await MatrixClientPeg.get().getProfileInfo(
this.props.verifier.userId,
);
this.setState({
opponentProfile: prof,
});
} catch (e) {
this.setState({
opponentProfileError: e,
});
}
}
_onFinished = () => { _onFinished = () => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED); this.props.onFinished(this.state.phase === PHASE_VERIFIED);
} }
@ -93,10 +112,39 @@ export default class IncomingSasDialog extends React.Component {
_renderPhaseStart() { _renderPhaseStart() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent("views.elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
let profile;
if (this.state.opponentProfile) {
profile = <div className="mx_IncomingSasDialog_opponentProfile">
<BaseAvatar name={this.state.opponentProfile.displayname}
idName={this.props.verifier.userId}
url={MatrixClientPeg.get().mxcUrlToHttp(
this.state.opponentProfile.avatar_url,
Math.floor(48 * window.devicePixelRatio),
Math.floor(48 * window.devicePixelRatio),
'crop',
)}
width={48} height={48} resizeMethod='crop'
/>
<h2>{this.state.opponentProfile.displayname}</h2>
</div>;
} else if (this.state.opponentProfileError) {
profile = <div>
<BaseAvatar name={this.props.verifier.userId.slice(1)}
idName={this.props.verifier.userId}
width={48} height={48}
/>
<h2>{this.props.verifier.userId}</h2>
</div>;
} else {
profile = <Spinner />;
}
return ( return (
<div> <div>
<h2>{this.props.verifier.userId}</h2> {profile}
<p>{_t( <p>{_t(
"Verify this user to mark them as trusted. " + "Verify this user to mark them as trusted. " +
"Trusting users gives you extra peace of mind when using " + "Trusting users gives you extra peace of mind when using " +

View file

@ -22,6 +22,10 @@ import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
export default class LogoutDialog extends React.Component { export default class LogoutDialog extends React.Component {
defaultProps = {
onFinished: function() {},
}
constructor() { constructor() {
super(); super();
this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this); this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this);
@ -29,14 +33,38 @@ export default class LogoutDialog extends React.Component {
this._onFinished = this._onFinished.bind(this); this._onFinished = this._onFinished.bind(this);
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this); this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
this._onLogoutConfirm = this._onLogoutConfirm.bind(this); this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
this.state = {
loading: false,
backupInfo: null,
error: null,
};
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
this._loadBackupStatus();
}
}
async _loadBackupStatus() {
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.setState({
loading: false,
backupInfo,
});
} catch (e) {
console.log("Unable to fetch key backup status", e);
this.setState({
loading: false,
error: e,
});
}
} }
_onSettingsLinkClick() { _onSettingsLinkClick() {
// close dialog // close dialog
if (this.props.onFinished) {
this.props.onFinished(); this.props.onFinished();
} }
}
_onExportE2eKeysClicked() { _onExportE2eKeysClicked() {
Modal.createTrackedDialogAsync('Export E2E Keys', '', Modal.createTrackedDialogAsync('Export E2E Keys', '',
@ -52,10 +80,8 @@ export default class LogoutDialog extends React.Component {
dis.dispatch({action: 'logout'}); dis.dispatch({action: 'logout'});
} }
// close dialog // close dialog
if (this.props.onFinished) {
this.props.onFinished(); this.props.onFinished();
} }
}
_onSetRecoveryMethodClick() { _onSetRecoveryMethodClick() {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
@ -63,72 +89,83 @@ export default class LogoutDialog extends React.Component {
); );
// close dialog // close dialog
if (this.props.onFinished) {
this.props.onFinished(); this.props.onFinished();
} }
}
_onLogoutConfirm() { _onLogoutConfirm() {
dis.dispatch({action: 'logout'}); dis.dispatch({action: 'logout'});
// close dialog // close dialog
if (this.props.onFinished) {
this.props.onFinished(); this.props.onFinished();
} }
}
render() { render() {
const description = <div> const description = <div>
<p>{_t( <p>{_t(
"When you log out, you'll lose your secure message history. To prevent " + "Encrypted messages are secured with end-to-end encryption. " +
"this, set up a recovery method.", "Only you and the recipient(s) have the keys to read these messages.",
)}</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> )}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>; </div>;
if (!MatrixClientPeg.get().getKeyBackupEnabled()) { if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let dialogContent;
if (this.state.loading) {
const Spinner = sdk.getComponent('views.elements.Spinner');
dialogContent = <Spinner />;
} else {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
// Not quite a standard question dialog as the primary button cancels let setupButtonCaption;
// the action and does something else instead, whilst non-default button if (this.state.backupInfo) {
// confirms the action. setupButtonCaption = _t("Use Key Backup");
return (<BaseDialog } else {
title={_t("Warning!")} // if there's an error fetching the backup info, we'll just assume there's
contentId='mx_Dialog_content' // no backup for the purpose of the button caption
hasCancel={false} setupButtonCaption = _t("Start using Key Backup");
onFinsihed={this._onFinished} }
>
dialogContent = <div>
<div className="mx_Dialog_content" id='mx_Dialog_content'> <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ description } { description }
</div> </div>
<DialogButtons primaryButton={_t('Set a Recovery Method')} <DialogButtons primaryButton={setupButtonCaption}
hasCancel={false} hasCancel={false}
onPrimaryButtonClick={this._onSetRecoveryMethodClick} onPrimaryButtonClick={this._onSetRecoveryMethodClick}
focus={true} focus={true}
> >
<button onClick={this._onLogoutConfirm}> <button onClick={this._onLogoutConfirm}>
{_t("I understand, log out without")} {_t("I don't want my encrypted messages")}
</button> </button>
</DialogButtons> </DialogButtons>
<details>
<summary>{_t("Advanced")}</summary>
<p><button onClick={this._onExportE2eKeysClicked}>
{_t("Manually export keys")}
</button></p>
</details>
</div>;
}
// 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("You'll lose access to your encrypted messages")}
contentId='mx_Dialog_content'
hasCancel={true}
onFinished={this._onFinished}
>
{dialogContent}
</BaseDialog>); </BaseDialog>);
} else { } else {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (<QuestionDialog return (<QuestionDialog
hasCancelButton={true} hasCancelButton={true}
title={_t("Sign out")} title={_t("Sign out")}
// TODO: This is made up by me and would need to also mention verifying
// once you can restore a backup by verifying a device
description={_t( description={_t(
"When signing in again, you can access encrypted chat history by " + "Are you sure you want to sign out?",
"restoring your key backup. You'll need your recovery passphrase " +
"or, if you didn't set a recovery passphrase, your recovery key " +
"(that you downloaded).",
)} )}
button={_t("Sign out")} button={_t("Sign out")}
onFinished={this._onFinished} onFinished={this._onFinished}

View file

@ -25,35 +25,12 @@ import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { markAllDevicesKnown } from '../../../cryptodevices'; import { markAllDevicesKnown } from '../../../cryptodevices';
function DeviceListEntry(props) {
const {userId, device} = props;
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
return (
<li>
{ device.deviceId }
<DeviceVerifyButtons device={device} userId={userId} />
<br />
{ device.getDisplayName() }
</li>
);
}
DeviceListEntry.propTypes = {
userId: PropTypes.string.isRequired,
// deviceinfo
device: PropTypes.object.isRequired,
};
function UserUnknownDeviceList(props) { function UserUnknownDeviceList(props) {
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
const {userId, userDevices} = props; const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) => const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<DeviceListEntry key={deviceId} userId={userId} <li key={deviceId}><MemberDeviceInfo device={userDevices[deviceId]} userId={userId} showDeviceId={true} /></li>,
device={userDevices[deviceId]} />,
); );
return ( return (

View file

@ -230,10 +230,15 @@ export default React.createClass({
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter Recovery Passphrase"); title = _t("Enter Recovery Passphrase");
content = <div> content = <div>
{_t( <p>{_t(
"<b>Warning</b>: you should only set up key backup " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and set up secure " + "Access your secure message history and set up secure " +
"messaging by entering your recovery passphrase.", "messaging by entering your recovery passphrase.",
)}<br /> )}</p>
<div className="mx_RestoreKeyBackupDialog_primaryContainer"> <div className="mx_RestoreKeyBackupDialog_primaryContainer">
<input type="password" <input type="password"
@ -288,10 +293,15 @@ export default React.createClass({
} }
content = <div> content = <div>
{_t( <p>{_t(
"<b>Warning</b>: you should only set up key backup " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and set up secure " + "Access your secure message history and set up secure " +
"messaging by entering your recovery key.", "messaging by entering your recovery key.",
)}<br /> )}</p>
<div className="mx_RestoreKeyBackupDialog_primaryContainer"> <div className="mx_RestoreKeyBackupDialog_primaryContainer">
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput" <input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"

View file

@ -579,8 +579,8 @@ export default class AppTile extends React.Component {
// editing is done in scalar // editing is done in scalar
const canUserModify = this._canUserModify(); const canUserModify = this._canUserModify();
const showEditButton = Boolean(this._scalarClient && canUserModify); const showEditButton = Boolean(this._scalarClient && canUserModify);
const showDeleteButton = canUserModify; const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
const showCancelButton = !showDeleteButton; const showCancelButton = (this.props.showCancel === undefined || this.props.showCancel) && !showDeleteButton;
// Picture snapshot - only show button when apps are maximised. // Picture snapshot - only show button when apps are maximised.
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
const showMinimiseButton = this.props.showMinimise && this.props.show; const showMinimiseButton = this.props.showMinimise && this.props.show;

View file

@ -16,6 +16,7 @@ limitations under the License.
import classNames from 'classnames'; import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
export default function(props) { export default function(props) {
const isWarning = props.status === "warning"; const isWarning = props.status === "warning";
@ -35,5 +36,10 @@ export default function(props) {
_t("All devices for this user are trusted") : _t("All devices for this user are trusted") :
_t("All devices in this encrypted room are trusted"); _t("All devices in this encrypted room are trusted");
} }
return (<div className={e2eIconClasses} title={e2eTitle} />); const icon = (<div className={e2eIconClasses} title={e2eTitle} />);
if (props.onClick) {
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
} else {
return icon;
}
} }

View file

@ -327,6 +327,7 @@ module.exports = withMatrixClient(React.createClass({
top: y, top: y,
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined, eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined, collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
e2eInfoCallback: () => this.onCryptoClicked(),
onFinished: function() { onFinished: function() {
self.setState({menu: false}); self.setState({menu: false});
}, },
@ -773,29 +774,31 @@ module.exports.haveTileForEvent = function(e) {
function E2ePadlockUndecryptable(props) { function E2ePadlockUndecryptable(props) {
return ( return (
<E2ePadlock title={_t("Undecryptable")} icon="undecryptable" /> <E2ePadlock title={_t("Undecryptable")} icon="undecryptable" {...props} />
); );
} }
function E2ePadlockUnverified(props) { function E2ePadlockUnverified(props) {
return ( return (
<E2ePadlock title={_t("Encrypted by an unverified device")} icon="unverified" /> <E2ePadlock title={_t("Encrypted by an unverified device")} icon="unverified" {...props} />
); );
} }
function E2ePadlockUnencrypted(props) { function E2ePadlockUnencrypted(props) {
return ( return (
<E2ePadlock title={_t("Unencrypted message")} icon="unencrypted" /> <E2ePadlock title={_t("Unencrypted message")} icon="unencrypted" {...props} />
); );
} }
function E2ePadlock(props) { function E2ePadlock(props) {
if (SettingsStore.getValue("alwaysShowEncryptionIcons")) { if (SettingsStore.getValue("alwaysShowEncryptionIcons")) {
return <div return (<div
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${props.icon}`} className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${props.icon}`}
title={props.title} onClick={props.onClick} />; title={props.title} onClick={props.onClick} />);
} else { } else {
return <div className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" onClick={props.onClick} />; return (<div
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden mx_EventTile_e2eIcon_${props.icon}`}
onClick={props.onClick} />);
} }
} }

View file

@ -30,7 +30,7 @@ export default class MemberDeviceInfo extends React.Component {
mx_MemberDeviceInfo_icon_unverified: this.props.device.isUnverified(), mx_MemberDeviceInfo_icon_unverified: this.props.device.isUnverified(),
}); });
const indicator = (<div className={iconClasses} />); const indicator = (<div className={iconClasses} />);
const deviceName = this.props.device.ambiguous ? const deviceName = (this.props.device.ambiguous || this.props.showDeviceId) ?
(this.props.device.getDisplayName() ? this.props.device.getDisplayName() : "") + " (" + this.props.device.deviceId + ")" : (this.props.device.getDisplayName() ? this.props.device.getDisplayName() : "") + " (" + this.props.device.deviceId + ")" :
this.props.device.getDisplayName(); this.props.device.getDisplayName();

View file

@ -941,6 +941,8 @@ module.exports = withMatrixClient(React.createClass({
} }
let roomMemberDetails = null; let roomMemberDetails = null;
let e2eIconElement;
if (this.props.member.roomId) { // is in room if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector'); const PowerSelector = sdk.getComponent('elements.PowerSelector');
roomMemberDetails = <div> roomMemberDetails = <div>
@ -959,6 +961,11 @@ module.exports = withMatrixClient(React.createClass({
{statusLabel} {statusLabel}
</div> </div>
</div>; </div>;
const isEncrypted = this.props.matrixClient.isRoomEncrypted(this.props.member.roomId);
if (this.state.e2eStatus && isEncrypted) {
e2eIconElement = (<E2EIcon status={this.state.e2eStatus} isUser={true} />);
}
} }
const avatarUrl = this.props.member.getMxcAvatarUrl(); const avatarUrl = this.props.member.getMxcAvatarUrl();
@ -967,7 +974,7 @@ module.exports = withMatrixClient(React.createClass({
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800); const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800);
avatarElement = <div className="mx_MemberInfo_avatar"> avatarElement = <div className="mx_MemberInfo_avatar">
<img src={httpUrl} /> <img src={httpUrl} />
</div> </div>;
} }
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
@ -979,7 +986,7 @@ module.exports = withMatrixClient(React.createClass({
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
<img src={require("../../../../res/img/minimise.svg")} width="10" height="16" className="mx_filterFlipColor" alt={_t('Close')} /> <img src={require("../../../../res/img/minimise.svg")} width="10" height="16" className="mx_filterFlipColor" alt={_t('Close')} />
</AccessibleButton> </AccessibleButton>
{ this.state.e2eStatus ? <E2EIcon status={this.state.e2eStatus} isUser={true} /> : undefined } { e2eIconElement }
<EmojiText element="h2">{ memberName }</EmojiText> <EmojiText element="h2">{ memberName }</EmojiText>
</div> </div>
{ avatarElement } { avatarElement }

View file

@ -0,0 +1,109 @@
/*
Copyright 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from "react";
import dis from "../../../dispatcher";
import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
import RoomAvatar from '../avatars/RoomAvatar';
import classNames from 'classnames';
const MAX_ROOMS = 20;
export default class RoomBreadcrumbs extends React.Component {
constructor(props) {
super(props);
this.state = {rooms: []};
this.onAction = this.onAction.bind(this);
this._previousRoomId = null;
this._dispatcherRef = null;
}
componentWillMount() {
this._dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
}
componentDidUpdate() {
const rooms = this.state.rooms.slice();
if (rooms.length) {
const {room, animated} = rooms[0];
if (!animated) {
rooms[0] = {room, animated: true};
setTimeout(() => this.setState({rooms}), 0);
}
}
}
onAction(payload) {
switch (payload.action) {
case 'view_room':
if (this._previousRoomId) {
this._appendRoomId(this._previousRoomId);
}
this._previousRoomId = payload.room_id;
}
}
_appendRoomId(roomId) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
return;
}
const rooms = this.state.rooms.slice();
const existingIdx = rooms.findIndex((r) => r.room.roomId === room.roomId);
if (existingIdx !== -1) {
rooms.splice(existingIdx, 1);
}
rooms.splice(0, 0, {room, animated: false});
if (rooms.length > MAX_ROOMS) {
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
}
this.setState({rooms});
}
_viewRoom(room) {
dis.dispatch({action: "view_room", room_id: room.roomId});
}
render() {
// check for collapsed here and
// not at parent so we keep
// rooms in our state
// when collapsing and expanding
if (this.props.collapsed) {
return null;
}
const rooms = this.state.rooms;
const avatars = rooms.map(({room, animated}, i) => {
const isFirst = i === 0;
const classes = classNames({
"mx_RoomBreadcrumbs_preAnimate": isFirst && !animated,
"mx_RoomBreadcrumbs_animate": isFirst,
});
return (
<AccessibleButton className={classes} key={room.roomId} title={room.name} onClick={() => this._viewRoom(room)}>
<RoomAvatar room={room} width={32} height={32} />
</AccessibleButton>
);
});
return (<div className="mx_RoomBreadcrumbs">{ avatars }</div>);
}
}

View file

@ -32,6 +32,7 @@ import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import * as cryptodevices from '../../../cryptodevices';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomHeader', displayName: 'RoomHeader',
@ -145,6 +146,12 @@ module.exports = React.createClass({
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0); return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
}, },
_onShowDevicesClick: function() {
if (this.props.e2eStatus === "warning") {
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
}
},
render: function() { render: function() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
@ -156,7 +163,7 @@ module.exports = React.createClass({
let pinnedEventsButton = null; let pinnedEventsButton = null;
const e2eIcon = this.props.e2eStatus ? const e2eIcon = this.props.e2eStatus ?
<E2EIcon status={this.props.e2eStatus} /> : <E2EIcon status={this.props.e2eStatus} onClick={this._onShowDevicesClick} /> :
undefined; undefined;
if (this.props.onCancelClick) { if (this.props.onCancelClick) {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import Timer from "../../../utils/Timer";
const React = require("react"); const React = require("react");
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
@ -41,6 +42,7 @@ import {Resizer} from '../../../resizer';
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
const HIDE_CONFERENCE_CHANS = true; const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
const HOVER_MOVE_TIMEOUT = 1000;
function labelForTagName(tagName) { function labelForTagName(tagName) {
if (tagName.startsWith('u.')) return tagName.slice(2); if (tagName.startsWith('u.')) return tagName.slice(2);
@ -73,6 +75,7 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
this._hoverClearTimer = null;
this._subListRefs = { this._subListRefs = {
// key => RoomSubList ref // key => RoomSubList ref
}; };
@ -357,11 +360,32 @@ module.exports = React.createClass({
this.forceUpdate(); this.forceUpdate();
}, },
onMouseEnter: function(ev) { onMouseMove: async function(ev) {
if (!this._hoverClearTimer) {
this.setState({hover: true}); this.setState({hover: true});
this._hoverClearTimer = new Timer(HOVER_MOVE_TIMEOUT);
this._hoverClearTimer.start();
let finished = true;
try {
await this._hoverClearTimer.finished();
} catch (err) {
finished = false;
}
this._hoverClearTimer = null;
if (finished) {
this.setState({hover: false});
this._delayedRefreshRoomList();
}
} else {
this._hoverClearTimer.restart();
}
}, },
onMouseLeave: function(ev) { onMouseLeave: function(ev) {
if (this._hoverClearTimer) {
this._hoverClearTimer.abort();
this._hoverClearTimer = null;
}
this.setState({hover: false}); this.setState({hover: false});
// Refresh the room list just in case the user missed something. // Refresh the room list just in case the user missed something.
@ -774,7 +798,7 @@ module.exports = React.createClass({
return ( return (
<div ref={this._collectResizeContainer} className="mx_RoomList" <div ref={this._collectResizeContainer} className="mx_RoomList"
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave}>
{ subListComponents } { subListComponents }
</div> </div>
); );

View file

@ -39,6 +39,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
loading: true, loading: true,
error: null, error: null,
backupInfo: null, backupInfo: null,
notNowClicked: false,
}; };
} }
@ -77,6 +78,10 @@ export default class RoomRecoveryReminder extends React.PureComponent {
} }
} }
onOnNotNowClick = () => {
this.setState({notNowClicked: true});
}
onDontAskAgainClick = () => { onDontAskAgainClick = () => {
// When you choose "Don't ask again" from the room reminder, we show a // When you choose "Don't ask again" from the room reminder, we show a
// dialog to confirm the choice. // dialog to confirm the choice.
@ -104,46 +109,54 @@ export default class RoomRecoveryReminder extends React.PureComponent {
} }
render() { render() {
if (this.state.loading) { // If there was an error loading just don't display the banner: we'll try again
// next time the user switchs to the room.
if (this.state.error || this.state.loading || this.state.notNowClicked) {
return null; return null;
} }
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
let body; let setupCaption;
if (this.state.error) { if (this.state.backupInfo) {
body = <div className="error"> setupCaption = _t("Use Key Backup");
{_t("Unable to load key backup status")}
</div>;
} else if (this.state.backupInfo) {
// A key backup exists for this account, but we're not using it.
body = <div>
<p>{_t(
"Secure Key Backup should be active on all of your devices to avoid " +
"losing access to your encrypted messages.",
)}</p>
</div>;
} else { } else {
body = _t( setupCaption = _t("Start using Key Backup");
"Securely back up your decryption keys to the server to make sure " +
"you'll always be able to read your encrypted messages.",
);
} }
return ( return (
<div className="mx_RoomRecoveryReminder"> <div className="mx_RoomRecoveryReminder">
<div className="mx_RoomRecoveryReminder_header">{_t( <div className="mx_RoomRecoveryReminder_header">{_t(
"Don't risk losing your encrypted messages!", "Never lose encrypted messages",
)}</div> )}</div>
<div className="mx_RoomRecoveryReminder_body">{body}</div> <div className="mx_RoomRecoveryReminder_body">
<p>{_t(
"Messages in this room are secured with end-to-end " +
"encryption. Only you and the recipient(s) have the " +
"keys to read these messages.",
)}</p>
<p>{_t(
"Securely back up your keys to avoid losing them. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => '',
},
)}</p>
</div>
<div className="mx_RoomRecoveryReminder_buttons"> <div className="mx_RoomRecoveryReminder_buttons">
<AccessibleButton className="mx_RoomRecoveryReminder_button" <AccessibleButton className="mx_RoomRecoveryReminder_button"
onClick={this.onSetupClick}> onClick={this.onSetupClick}>
{_t("Activate Secure Key Backup")} {setupCaption}
</AccessibleButton> </AccessibleButton>
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onOnNotNowClick}>
{ _t("Not now") }
</AccessibleButton></p>
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton" <p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onDontAskAgainClick}> onClick={this.onDontAskAgainClick}>
{ _t("No thanks, I'll download a copy of my decryption keys before I log out") } { _t("Don't ask me again") }
</AccessibleButton></p> </AccessibleButton></p>
</div> </div>
</div> </div>

View file

@ -108,13 +108,6 @@ module.exports = React.createClass({
return statusUser._unstable_statusMessage; return statusUser._unstable_statusMessage;
}, },
onRoomTimeline: function(ev, room) {
if (room !== this.props.room) return;
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
},
onRoomName: function(room) { onRoomName: function(room) {
if (room !== this.props.room) return; if (room !== this.props.room) return;
this.setState({ this.setState({
@ -159,7 +152,6 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
MatrixClientPeg.get().on("accountData", this.onAccountData); MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("Room.name", this.onRoomName);
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange); ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
@ -179,7 +171,6 @@ module.exports = React.createClass({
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
} }
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange); ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
@ -306,7 +297,7 @@ module.exports = React.createClass({
render: function() { render: function() {
const isInvite = this.props.room.getMyMembership() === "invite"; const isInvite = this.props.room.getMyMembership() === "invite";
const notificationCount = this.state.notificationCount; const notificationCount = this.props.notificationCount;
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); // var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(); const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();

View file

@ -226,6 +226,7 @@ export default class Stickerpicker extends React.Component {
showTitle={false} showTitle={false}
showMinimise={true} showMinimise={true}
showDelete={false} showDelete={false}
showCancel={false}
showPopout={false} showPopout={false}
onMinimiseClick={this._onHideStickersClick} onMinimiseClick={this._onHideStickersClick}
handleMinimisePointerEvents={true} handleMinimisePointerEvents={true}

View file

@ -111,10 +111,10 @@ export default class KeyBackupPanel extends React.PureComponent {
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
title: _t('Delete Backup'), title: _t('Delete Backup'),
description: _t( description: _t(
"Delete your backed up encryption keys from the server? " + "Are you sure? You will lose your encrypted messages if your " +
"You will no longer be able to use your recovery key to read encrypted message history", "keys are not backed up properly.",
), ),
button: _t('Delete backup'), button: _t('Delete Backup'),
danger: true, danger: true,
onFinished: (proceed) => { onFinished: (proceed) => {
if (!proceed) return; if (!proceed) return;
@ -135,6 +135,10 @@ export default class KeyBackupPanel extends React.PureComponent {
render() { render() {
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const encryptedMessageAreEncrypted = _t(
"Encrypted messages are secured with end-to-end encryption. " +
"Only you and the recipient(s) have the keys to read these messages.",
);
if (this.state.error) { if (this.state.error) {
return ( return (
@ -145,14 +149,25 @@ export default class KeyBackupPanel extends React.PureComponent {
} else if (this.state.loading) { } else if (this.state.loading) {
return <Spinner />; return <Spinner />;
} else if (this.state.backupInfo) { } else if (this.state.backupInfo) {
const EmojiText = sdk.getComponent('elements.EmojiText');
let clientBackupStatus; let clientBackupStatus;
let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.get().getKeyBackupEnabled()) { if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = _t("This device is using key backup"); clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t("This device is backing up your keys. ")}<EmojiText></EmojiText></p>
</div>;
} else { } else {
clientBackupStatus = _t( clientBackupStatus = <div>
"This device is <b>not</b> using key backup. Restore the backup to start using it.", {}, <p>{encryptedMessageAreEncrypted}</p>
{b: x => <b>{x}</b>}, <p>{_t(
); "This device is <b>not backing up your keys</b>.", {},
{b: sub => <b>{sub}</b>},
)}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>;
restoreButtonCaption = _t("Use key backup");
} }
let uploadStatus; let uploadStatus;
@ -243,18 +258,25 @@ export default class KeyBackupPanel extends React.PureComponent {
</details> </details>
<p> <p>
<AccessibleButton kind="primary" onClick={this._restoreBackup}> <AccessibleButton kind="primary" onClick={this._restoreBackup}>
{ _t("Restore backup") } {restoreButtonCaption}
</AccessibleButton>&nbsp;&nbsp;&nbsp; </AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton kind="danger" onClick={this._deleteBackup}> <AccessibleButton kind="danger" onClick={this._deleteBackup}>
{ _t("Delete backup") } { _t("Delete Backup") }
</AccessibleButton> </AccessibleButton>
</p> </p>
</div>; </div>;
} else { } else {
return <div> return <div>
{_t("No backup is present")}<br /><br /> <div>
<p>{_t(
"Your keys are <b>not being backed up from this device</b>.", {},
{b: sub => <b>{sub}</b>},
)}</p>
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>
<AccessibleButton kind="primary" onClick={this._startNewBackup}> <AccessibleButton kind="primary" onClick={this._startNewBackup}>
{ _t("Start a new backup") } { _t("Start using Key Backup") }
</AccessibleButton> </AccessibleButton>
</div>; </div>;
} }

View file

@ -270,6 +270,7 @@
"Failed to join room": "Failed to join room", "Failed to join room": "Failed to join room",
"Message Pinning": "Message Pinning", "Message Pinning": "Message Pinning",
"Custom user status messages": "Custom user status messages", "Custom user status messages": "Custom user status messages",
"Show recent room avatars above the room list (refresh to apply changes)": "Show recent room avatars above the room list (refresh to apply changes)",
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Render simple counters in room header": "Render simple counters in room header", "Render simple counters in room header": "Render simple counters in room header",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
@ -439,11 +440,14 @@
"Disable Notifications": "Disable Notifications", "Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications", "Enable Notifications": "Enable Notifications",
"Delete Backup": "Delete Backup", "Delete Backup": "Delete Backup",
"Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
"Delete backup": "Delete backup", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
"Unable to load key backup status": "Unable to load key backup status", "Unable to load key backup status": "Unable to load key backup status",
"This device is using key backup": "This device is using key backup", "Restore from Backup": "Restore from Backup",
"This device is <b>not</b> using key backup. Restore the backup to start using it.": "This device is <b>not</b> using key backup. Restore the backup to start using it.", "This device is backing up your keys. ": "This device is backing up your keys. ",
"This device is <b>not backing up your keys</b>.": "This device is <b>not backing up your keys</b>.",
"Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.",
"Use key backup": "Use key backup",
"Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...",
"All keys backed up": "All keys backed up", "All keys backed up": "All keys backed up",
"Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.": "Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.", "Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.": "Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.",
@ -457,9 +461,8 @@
"Advanced": "Advanced", "Advanced": "Advanced",
"Backup version: ": "Backup version: ", "Backup version: ": "Backup version: ",
"Algorithm: ": "Algorithm: ", "Algorithm: ": "Algorithm: ",
"Restore backup": "Restore backup", "Your keys are <b>not being backed up from this device</b>.": "Your keys are <b>not being backed up from this device</b>.",
"No backup is present": "No backup is present", "Start using Key Backup": "Start using Key Backup",
"Start a new backup": "Start a new backup",
"Error saving email notification preferences": "Error saving email notification preferences", "Error saving email notification preferences": "Error saving email notification preferences",
"An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.", "An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.",
"Keywords": "Keywords", "Keywords": "Keywords",
@ -785,11 +788,12 @@
"You are trying to access a room.": "You are trying to access a room.", "You are trying to access a room.": "You are trying to access a room.",
"<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!", "<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!",
"This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled",
"Secure Key Backup should be active on all of your devices to avoid losing access to your encrypted messages.": "Secure Key Backup should be active on all of your devices to avoid losing access to your encrypted messages.", "Use Key Backup": "Use Key Backup",
"Securely back up your decryption keys to the server to make sure you'll always be able to read your encrypted messages.": "Securely back up your decryption keys to the server to make sure you'll always be able to read your encrypted messages.", "Never lose encrypted messages": "Never lose encrypted messages",
"Don't risk losing your encrypted messages!": "Don't risk losing your encrypted messages!", "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
"Activate Secure Key Backup": "Activate Secure Key Backup", "Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
"No thanks, I'll download a copy of my decryption keys before I log out": "No thanks, I'll download a copy of my decryption keys before I log out", "Not now": "Not now",
"Don't ask me again": "Don't ask me again",
"Add a topic": "Add a topic", "Add a topic": "Add a topic",
"This room is using an unstable room version. If you aren't expecting this, please upgrade the room.": "This room is using an unstable room version. If you aren't expecting this, please upgrade the room.", "This room is using an unstable room version. If you aren't expecting this, please upgrade the room.": "This room is using an unstable room version. If you aren't expecting this, please upgrade the room.",
"Click here to upgrade to the latest room version.": "Click here to upgrade to the latest room version.", "Click here to upgrade to the latest room version.": "Click here to upgrade to the latest room version.",
@ -1061,6 +1065,7 @@
"Verify by comparing a short text string.": "Verify by comparing a short text string.", "Verify by comparing a short text string.": "Verify by comparing a short text string.",
"Begin Verifying": "Begin Verifying", "Begin Verifying": "Begin Verifying",
"Waiting for partner to accept...": "Waiting for partner to accept...", "Waiting for partner to accept...": "Waiting for partner to accept...",
"Nothing appearing? Not all clients support interactive verification yet. <button>Use legacy verification</button>.": "Nothing appearing? Not all clients support interactive verification yet. <button>Use legacy verification</button>.",
"Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...", "Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...",
"Use two-way text verification": "Use two-way text verification", "Use two-way text verification": "Use two-way text verification",
"To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:",
@ -1100,11 +1105,10 @@
"Clear cache and resync": "Clear cache and resync", "Clear cache and resync": "Clear cache and resync",
"Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating Riot": "Updating Riot", "Updating Riot": "Updating Riot",
"When you log out, you'll lose your secure message history. To prevent this, set up a recovery method.": "When you log out, you'll lose your secure message history. To prevent this, set up a recovery method.", "I don't want my encrypted messages": "I don't want my encrypted messages",
"Alternatively, advanced users can also manually export encryption keys in <a>Settings</a> before logging out.": "Alternatively, advanced users can also manually export encryption keys in <a>Settings</a> before logging out.", "Manually export keys": "Manually export keys",
"Set a Recovery Method": "Set a Recovery Method", "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
"I understand, log out without": "I understand, log out without", "Are you sure you want to sign out?": "Are you sure you want to sign out?",
"When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery passphrase or, if you didn't set a recovery passphrase, your recovery key (that you downloaded).": "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery passphrase or, if you didn't set a recovery passphrase, your recovery key (that you downloaded).",
"Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.", "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.",
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.", "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
"Report bugs & give feedback": "Report bugs & give feedback", "Report bugs & give feedback": "Report bugs & give feedback",
@ -1171,6 +1175,7 @@
"Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!", "Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!",
"Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys", "Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys",
"Enter Recovery Passphrase": "Enter Recovery Passphrase", "Enter Recovery Passphrase": "Enter Recovery Passphrase",
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Warning</b>: you should only set up key backup from a trusted computer.",
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
"Next": "Next", "Next": "Next",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>", "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
@ -1216,6 +1221,7 @@
"Set status": "Set status", "Set status": "Set status",
"Set a new status...": "Set a new status...", "Set a new status...": "Set a new status...",
"View Community": "View Community", "View Community": "View Community",
"Hide": "Hide",
"Login": "Login", "Login": "Login",
"powered by Matrix": "powered by Matrix", "powered by Matrix": "powered by Matrix",
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>", "Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
@ -1487,19 +1493,19 @@
"Import": "Import", "Import": "Import",
"Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.",
"Keep going...": "Keep going...", "Keep going...": "Keep going...",
"Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.", "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
"You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.", "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Enter a passphrase...": "Enter a passphrase...", "Enter a passphrase...": "Enter a passphrase...",
"If you don't want encrypted message history to be available on other devices, <button>opt out</button>.": "If you don't want encrypted message history to be available on other devices, <button>opt out</button>.", "Set up with a Recovery Key": "Set up with a Recovery Key",
"Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.": "Or, if you don't want to create a Recovery Passphrase, skip this step and <button>download a recovery key</button>.",
"That matches!": "That matches!", "That matches!": "That matches!",
"That doesn't match.": "That doesn't match.", "That doesn't match.": "That doesn't match.",
"Go back to set it again.": "Go back to set it again.", "Go back to set it again.": "Go back to set it again.",
"Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.", "Please enter your passphrase a second time to confirm.": "Please enter your passphrase a second time to confirm.",
"Repeat your passphrase...": "Repeat your passphrase...", "Repeat your passphrase...": "Repeat your passphrase...",
"As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.", "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.",
"As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.", "As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.",
"Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.",
"Keep your recovery key somewhere very secure, like a password manager (or a safe)": "Keep your recovery key somewhere very secure, like a password manager (or a safe)",
"Your Recovery Key": "Your Recovery Key", "Your Recovery Key": "Your Recovery Key",
"Copy to clipboard": "Copy to clipboard", "Copy to clipboard": "Copy to clipboard",
"Download": "Download", "Download": "Download",
@ -1508,15 +1514,16 @@
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe", "<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive", "<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage", "<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
"Your encryption keys are now being backed up in the background to your Homeserver. The initial backup could take several minutes. You can view key backup upload progress in Settings.": "Your encryption keys are now being backed up in the background to your Homeserver. The initial backup could take several minutes. You can view key backup upload progress in Settings.", "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
"Okay": "Okay",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.",
"Set up Secure Message Recovery": "Set up Secure Message Recovery", "Set up Secure Message Recovery": "Set up Secure Message Recovery",
"Create a Recovery Passphrase": "Create a Recovery Passphrase", "Secure your backup with a passphrase": "Secure your backup with a passphrase",
"Confirm Recovery Passphrase": "Confirm Recovery Passphrase", "Confirm your passphrase": "Confirm your passphrase",
"Recovery Key": "Recovery Key", "Recovery key": "Recovery key",
"Keep it safe": "Keep it safe", "Keep it safe": "Keep it safe",
"Starting backup...": "Starting backup...", "Starting backup...": "Starting backup...",
"Backup Started": "Backup Started", "Success!": "Success!",
"Create Key Backup": "Create Key Backup", "Create Key Backup": "Create Key Backup",
"Unable to create key backup": "Unable to create key backup", "Unable to create key backup": "Unable to create key backup",
"Retry": "Retry", "Retry": "Retry",

View file

@ -20,54 +20,28 @@ limitations under the License.
* to update the interface once for all of them. * to update the interface once for all of them.
* *
* Note that the function must not take arguments, since the args * Note that the function must not take arguments, since the args
* could be different for each invocarion of the function. * could be different for each invocation of the function.
* *
* The returned function has a 'cancelPendingCall' property which can be called * The returned function has a 'cancelPendingCall' property which can be called
* on unmount or similar to cancel any pending update. * on unmount or similar to cancel any pending update.
*/ */
module.exports = function(f, minIntervalMs) {
this.lastCall = 0;
this.scheduledCall = undefined;
const self = this; import { throttle } from "lodash";
const wrapper = function() {
const now = Date.now();
if (self.lastCall < now - minIntervalMs) { export default function ratelimitedfunc(fn, time) {
f.apply(this); const throttledFn = throttle(fn, time, {
// get the time again now the function has finished, so if it leading: true,
// took longer than the delay time to execute, it doesn't trailing: true,
// immediately become eligible to run again. });
self.lastCall = Date.now(); const _bind = throttledFn.bind;
} else if (self.scheduledCall === undefined) { throttledFn.bind = function() {
self.scheduledCall = setTimeout( const boundFn = _bind.apply(throttledFn, arguments);
() => { boundFn.cancelPendingCall = throttledFn.cancelPendingCall;
self.scheduledCall = undefined; return boundFn;
f.apply(this); };
// get time again as per above
self.lastCall = Date.now(); throttledFn.cancelPendingCall = function() {
}, throttledFn.cancel();
(self.lastCall + minIntervalMs) - now, };
); return throttledFn;
} }
};
// add the cancelPendingCall property
wrapper.cancelPendingCall = function() {
if (self.scheduledCall) {
clearTimeout(self.scheduledCall);
self.scheduledCall = undefined;
}
};
// make sure that cancelPendingCall is copied when react rebinds the
// wrapper
const _bind = wrapper.bind;
wrapper.bind = function() {
const rebound = _bind.apply(this, arguments);
rebound.cancelPendingCall = wrapper.cancelPendingCall;
return rebound;
};
return wrapper;
};

View file

@ -99,6 +99,12 @@ export const SETTINGS = {
default: false, default: false,
controller: new CustomStatusController(), controller: new CustomStatusController(),
}, },
"feature_room_breadcrumbs": {
isFeature: true,
displayName: _td("Show recent room avatars above the room list (refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_custom_tags": { "feature_custom_tags": {
isFeature: true, isFeature: true,
displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"), displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),