diff --git a/.babelrc b/.babelrc index 6ba0e0dae0..fc5bd1788f 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { "presets": ["react", "es2015", "es2016"], - "plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports"] + "plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports", "syntax-dynamic-import"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index dae09ee2f1..eea47dcb8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,84 @@ +Changes in [0.14.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.6) (2018-11-22) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5...v0.14.6) + + * Warning when crypto DB is too new to use. + +Changes in [0.14.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.5) (2018-11-19) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5-rc.2...v0.14.5) + + * No changes since rc.1 + +Changes in [0.14.5-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.5-rc.2) (2018-11-15) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5-rc.1...v0.14.5-rc.2) + + * Update to js-sdk v0.14.0-rc.1 which uses the new Olm API + (v0.14.0-rc.1 was broken because of this). + +Changes in [0.14.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.5-rc.1) (2018-11-15) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.4...v0.14.5-rc.1) + + * Update from Weblate. + [\#2278](https://github.com/matrix-org/matrix-react-sdk/pull/2278) + * Support room IDs and event permalinks in the join command + [\#2272](https://github.com/matrix-org/matrix-react-sdk/pull/2272) + * Align encrypted event buttons in Safari + [\#2274](https://github.com/matrix-org/matrix-react-sdk/pull/2274) + * Align buttons in encrypted event dialog + [\#2273](https://github.com/matrix-org/matrix-react-sdk/pull/2273) + * Add visible guest warning to encourage login + [\#2268](https://github.com/matrix-org/matrix-react-sdk/pull/2268) + * Regenerate the room list when m.fully_read is issued + [\#2266](https://github.com/matrix-org/matrix-react-sdk/pull/2266) + * Remove the request-only stuff we don't need anymore + [\#2263](https://github.com/matrix-org/matrix-react-sdk/pull/2263) + * Improve performance of room list and fix timestamp ordering when pinning + rooms + [\#2265](https://github.com/matrix-org/matrix-react-sdk/pull/2265) + * Add options to pin unread/mentioned rooms to the top of the room list + [\#1936](https://github.com/matrix-org/matrix-react-sdk/pull/1936) + * only run e2e tests on PRs targeted on develop + [\#2261](https://github.com/matrix-org/matrix-react-sdk/pull/2261) + * Fix and test matrix.to alias permalinks + [\#2254](https://github.com/matrix-org/matrix-react-sdk/pull/2254) + * click-through svg on tag tile context menu to make it less weird + [\#2257](https://github.com/matrix-org/matrix-react-sdk/pull/2257) + * Hide Matthew's Time Machine + [\#2256](https://github.com/matrix-org/matrix-react-sdk/pull/2256) + * Update babel-eslint to 8.1.1 + [\#2255](https://github.com/matrix-org/matrix-react-sdk/pull/2255) + * Support routing matrix.to links to joinable rooms + [\#2250](https://github.com/matrix-org/matrix-react-sdk/pull/2250) + * Fix autoreplacement of ascii emoji + [\#2253](https://github.com/matrix-org/matrix-react-sdk/pull/2253) + * Repair DevTools button padding by centralizing styles + [\#2252](https://github.com/matrix-org/matrix-react-sdk/pull/2252) + * Redirect widgets to another location before deleting them + [\#2232](https://github.com/matrix-org/matrix-react-sdk/pull/2232) + * disable e2e tests for PRs targeted at experimental (redesign) + [\#2251](https://github.com/matrix-org/matrix-react-sdk/pull/2251) + * Fix emoji replacement in composer + [\#2247](https://github.com/matrix-org/matrix-react-sdk/pull/2247) + * Add a devtools button to roomsettings + [\#2249](https://github.com/matrix-org/matrix-react-sdk/pull/2249) + * Add warning when administrator leaves community (#5724) + [\#2242](https://github.com/matrix-org/matrix-react-sdk/pull/2242) + +Changes in [0.14.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.4) (2018-11-13) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.3...v0.14.4) + + * Include change that was supposed to be included in orevious version + +Changes in [0.14.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.3) (2018-11-13) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.2...v0.14.3) + + * Add banner with login/register links for users who aren't logged in + Changes in [0.14.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.2) (2018-10-29) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.2-rc.1...v0.14.2) diff --git a/jenkins.sh b/jenkins.sh index 8cf5ee4a1f..f4bb8da449 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -4,7 +4,7 @@ set -e export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -nvm use 6 +nvm use 10 set -x diff --git a/package.json b/package.json index 8a51c0877d..b5cdfdf401 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.2", + "version": "0.14.6", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -53,6 +53,7 @@ "test-multi": "karma start" }, "dependencies": { + "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-runtime": "^6.26.0", "bluebird": "^3.5.0", "blueimp-canvas-to-blob": "^3.5.0", @@ -75,7 +76,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "0.14.1", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", diff --git a/res/css/_common.scss b/res/css/_common.scss index a702571397..5b974baceb 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -254,6 +254,11 @@ textarea { opacity: 0.7; } +.mx_linkButton { + cursor: pointer; + color: $accent-color; +} + .mx_Dialog_title { min-height: 16px; padding-top: 40px; diff --git a/res/css/_components.scss b/res/css/_components.scss index d56f782ffb..92e243e8d1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -35,17 +35,21 @@ @import "./views/dialogs/_ChatInviteDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; +@import "./views/dialogs/_CreateKeyBackupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; +@import "./views/dialogs/_RestoreKeyBackupDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SetMxIdDialog.scss"; @import "./views/dialogs/_SetPasswordDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; +@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; +@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; @import "./views/elements/_AddressSelector.scss"; @@ -111,6 +115,7 @@ @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_IntegrationsManager.scss"; +@import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_Notifications.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_IncomingCallbox.scss"; diff --git a/res/css/structures/_HomePage.scss b/res/css/structures/_HomePage.scss index cdac1bcc8a..dc3158b39d 100644 --- a/res/css/structures/_HomePage.scss +++ b/res/css/structures/_HomePage.scss @@ -33,3 +33,16 @@ limitations under the License. .mx_HomePage_body { // margin-left: 63px; } + +.mx_HomePage_guest_warning { + display: flex; + background-color: $secondary-accent-color; + border: 1px solid $accent-color; + margin: 20px; + padding: 20px 40px; + border-radius: 5px; +} + +.mx_HomePage_guest_warning img { + padding-right: 10px; +} diff --git a/res/css/structures/login/_Login.scss b/res/css/structures/login/_Login.scss index 84b8306a74..1264d2a30f 100644 --- a/res/css/structures/login/_Login.scss +++ b/res/css/structures/login/_Login.scss @@ -142,6 +142,17 @@ limitations under the License. color: $primary-fg-color; } +.mx_Login_sso_link { + display: block; + text-align: center; + font-size: 15px; + margin-bottom: 20px; +} + +.mx_Login_sso_link:link { + color: $primary-fg-color; +} + .mx_Login_loader { display: inline; position: relative; diff --git a/res/css/views/dialogs/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/_CreateKeyBackupDialog.scss new file mode 100644 index 0000000000..a422cf858c --- /dev/null +++ b/res/css/views/dialogs/_CreateKeyBackupDialog.scss @@ -0,0 +1,25 @@ +/* +Copyright 2018 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_CreateKeyBackupDialog { + padding-right: 40px; +} + +.mx_CreateKeyBackupDialog_recoveryKey { + padding: 20px; + color: $info-plinth-fg-color; + background-color: $info-plinth-bg-color; +} diff --git a/res/css/views/dialogs/_EncryptedEventDialog.scss b/res/css/views/dialogs/_EncryptedEventDialog.scss index b4dd353370..58ed8b2527 100644 --- a/res/css/views/dialogs/_EncryptedEventDialog.scss +++ b/res/css/views/dialogs/_EncryptedEventDialog.scss @@ -24,4 +24,8 @@ limitations under the License. @mixin mx_DialogButton; background-color: $primary-bg-color; color: $accent-color; -} \ No newline at end of file +} + +.mx_EncryptedEventDialog button { + margin-top: 0px; +} diff --git a/res/css/views/dialogs/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/_RestoreKeyBackupDialog.scss new file mode 100644 index 0000000000..69e00c416a --- /dev/null +++ b/res/css/views/dialogs/_RestoreKeyBackupDialog.scss @@ -0,0 +1,19 @@ +/* +Copyright 2018 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_RestoreKeyBackupDialog_keyStatus { + height: 30px; +} diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss new file mode 100644 index 0000000000..507c89ace7 --- /dev/null +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -0,0 +1,39 @@ +/* +Copyright 2018 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_CreateKeyBackupDialog_primaryContainer { + /*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/ + padding: 20px +} + +.mx_CreateKeyBackupDialog_passPhraseInput { + width: 300px; + border: 1px solid $accent-color; + border-radius: 5px; + padding: 10px; +} + +.mx_CreateKeyBackupDialog_passPhraseMatch { + float: right; +} + +.mx_CreateKeyBackupDialog_recoveryKeyButtons { + float: right; +} + +.mx_CreateKeyBackupDialog_recoveryKey { + width: 300px; +} diff --git a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss new file mode 100644 index 0000000000..612c921038 --- /dev/null +++ b/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss @@ -0,0 +1,29 @@ +/* +Copyright 2018 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_RestoreKeyBackupDialog_primaryContainer { + /*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/ + padding: 20px +} + +.mx_RestoreKeyBackupDialog_passPhraseInput, +.mx_RestoreKeyBackupDialog_recoveryKeyInput { + width: 300px; + border: 1px solid $accent-color; + border-radius: 5px; + padding: 10px; +} + diff --git a/res/css/views/login/_InteractiveAuthEntryComponents.scss b/res/css/views/login/_InteractiveAuthEntryComponents.scss index 183b5cd251..e2ea7d86fb 100644 --- a/res/css/views/login/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/login/_InteractiveAuthEntryComponents.scss @@ -35,8 +35,24 @@ limitations under the License. margin-bottom: 5px; } +.mx_InteractiveAuthEntryComponents_termsSubmit { + margin-top: 20px; + margin-bottom: 5px; + display: block; + width: 100%; +} + // XXX: This should be a common button class .mx_InteractiveAuthEntryComponents_msisdnSubmit:disabled { background-color: $light-fg-color; cursor: default; } + +.mx_InteractiveAuthEntryComponents_termsSubmit:disabled { + background-color: $accent-color-50pct; + cursor: default; +} + +.mx_InteractiveAuthEntryComponents_termsPolicy { + display: block; +} \ No newline at end of file diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 4c763c5991..a5a1e66a3b 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -46,3 +46,14 @@ limitations under the License. .mx_MImageBody_thumbnail_spinner > * { transform: translate(-50%, -50%); } + +.mx_MImageBody_gifLabel { + position: absolute; + display: block; + top: 0px; + left: 14px; + padding: 5px; + border-radius: 5px; + background: $imagebody-giflabel; + border: 2px solid $imagebody-giflabel-border; +} diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss index 5888820e0d..d4e4bb1adf 100644 --- a/res/css/views/rooms/_MemberDeviceInfo.scss +++ b/res/css/views/rooms/_MemberDeviceInfo.scss @@ -19,7 +19,6 @@ limitations under the License. } .mx_MemberDeviceInfo.mx_DeviceVerifyButtons { - padding: 6px 0; display: flex; flex-wrap: wrap; justify-content: space-between; diff --git a/src/components/views/login/CasLogin.js b/res/css/views/settings/_KeyBackupPanel.scss similarity index 52% rename from src/components/views/login/CasLogin.js rename to res/css/views/settings/_KeyBackupPanel.scss index 9219c79733..1bcc0ab10d 100644 --- a/src/components/views/login/CasLogin.js +++ b/res/css/views/settings/_KeyBackupPanel.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 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. @@ -14,25 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +.mx_KeyBackupPanel_sigValid, .mx_KeyBackupPanel_sigInvalid, +.mx_KeyBackupPanel_deviceVerified, .mx_KeyBackupPanel_deviceNotVerified { + font-weight: bold; +} -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; +.mx_KeyBackupPanel_sigValid, .mx_KeyBackupPanel_deviceVerified { + color: $e2e-verified-color; +} -module.exports = React.createClass({ - displayName: 'CasLogin', +.mx_KeyBackupPanel_sigInvalid, .mx_KeyBackupPanel_deviceNotVerified { + color: $e2e-warning-color; +} - propTypes: { - onSubmit: PropTypes.func, // fn() - }, - - render: function() { - return ( -
- -
- ); - }, - -}); +.mx_KeyBackupPanel_deviceName { + font-style: italic; +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 41ec0f8fd6..7bb9fcc053 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -12,6 +12,7 @@ $light-fg-color: #747474; // button UI (white-on-green in light skin) $accent-fg-color: $primary-bg-color; $accent-color: #76CFA6; +$accent-color-50pct: #76CFA67F; $selection-fg-color: $primary-fg-color; @@ -158,6 +159,9 @@ $lightbox-bg-color: #454545; $lightbox-fg-color: #ffffff; $lightbox-border-color: #ffffff; +$imagebody-giflabel: rgba(1, 1, 1, 0.7); +$imagebody-giflabel-border: rgba(1, 1, 1, 0.2); + // unused? $progressbar-color: #000; diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 4c41037229..3aaa1dde2a 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -20,6 +20,7 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; $accent-color: #76CFA6; +$accent-color-50pct: #76CFA67F; $selection-fg-color: $primary-bg-color; @@ -173,6 +174,9 @@ $lightbox-bg-color: #454545; $lightbox-fg-color: #ffffff; $lightbox-border-color: #ffffff; +$imagebody-giflabel: rgba(0, 0, 0, 0.7); +$imagebody-giflabel-border: rgba(0, 0, 0, 0.2); + // unused? $progressbar-color: #000; diff --git a/src/Login.js b/src/Login.js index 61a14959d8..ec55a1e8c7 100644 --- a/src/Login.js +++ b/src/Login.js @@ -16,7 +16,6 @@ limitations under the License. */ import Matrix from "matrix-js-sdk"; -import { _t } from "./languageHandler"; import Promise from 'bluebird'; import url from 'url'; @@ -225,19 +224,18 @@ export default class Login { }); } - redirectToCas() { + getSsoLoginUrl(loginType) { const client = this._createTemporaryClient(); const parsedUrl = url.parse(window.location.href, true); // XXX: at this point, the fragment will always be #/login, which is no // use to anyone. Ideally, we would get the intended fragment from // MatrixChat.screenAfterLogin so that you could follow #/room links etc - // through a CAS login. + // through an SSO login. parsedUrl.hash = ""; parsedUrl.query["homeserver"] = client.getHomeserverUrl(); parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); - const casUrl = client.getCasLoginUrl(url.format(parsedUrl)); - window.location.href = casUrl; + return client.getSsoLoginUrl(url.format(parsedUrl), loginType); } } diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 04b3b47e43..9a77901d2e 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -23,10 +23,12 @@ import Matrix from 'matrix-js-sdk'; import utils from 'matrix-js-sdk/lib/utils'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; +import sdk from './index'; import createMatrixClient from './utils/createMatrixClient'; import SettingsStore from './settings/SettingsStore'; import MatrixActionCreators from './actions/MatrixActionCreators'; import {phasedRollOutExpiredForUser} from "./PhasedRollOut"; +import Modal from './Modal'; interface MatrixClientCreds { homeserverUrl: string, @@ -116,6 +118,14 @@ class MatrixClientPeg { await this.matrixClient.initCrypto(); } } catch (e) { + if (e.name === 'InvalidCryptoStoreError') { + // The js-sdk found a crypto DB too new for it to use + const CryptoStoreTooNewDialog = + sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); + Modal.createDialog(CryptoStoreTooNewDialog, { + host: window.location.host, + }); + } // this can happen for a number of reasons, the most likely being // that the olm library was missing. It's not fatal. console.warn("Unable to initialise e2e: " + e); diff --git a/src/Modal.js b/src/Modal.js index 06a96824a7..960e0e5c30 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -23,6 +23,7 @@ import PropTypes from 'prop-types'; import Analytics from './Analytics'; import sdk from './index'; import dis from './dispatcher'; +import { _t } from './languageHandler'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; @@ -32,15 +33,15 @@ const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; */ const AsyncWrapper = React.createClass({ propTypes: { - /** A function which takes a 'callback' argument which it will call - * with the real component once it loads. + /** A promise which resolves with the real component */ - loader: PropTypes.func.isRequired, + prom: PropTypes.object.isRequired, }, getInitialState: function() { return { component: null, + error: null, }; }, @@ -49,14 +50,18 @@ const AsyncWrapper = React.createClass({ // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('Starting load of AsyncWrapper for modal'); - this.props.loader((e) => { - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('AsyncWrapper load completed with '+e.displayName); + this.props.prom.then((result) => { if (this._unmounted) { return; } - this.setState({component: e}); + // Take the 'default' member if it's there, then we support + // passing in just an import()ed module, since ES6 async import + // always returns a module *namespace*. + const component = result.default ? result.default : result; + this.setState({component}); + }).catch((e) => { + console.warn('AsyncWrapper promise failed', e); + this.setState({error: e}); }); }, @@ -64,11 +69,27 @@ const AsyncWrapper = React.createClass({ this._unmounted = true; }, + _onWrapperCancelClick: function() { + this.props.onFinished(false); + }, + render: function() { const {loader, ...otherProps} = this.props; if (this.state.component) { const Component = this.state.component; return ; + } else if (this.state.error) { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return + {_t("Unable to load! Check your network connectivity and try again.")} + + ; } else { // show a spinner until the component is loaded. const Spinner = sdk.getComponent("elements.Spinner"); @@ -115,7 +136,7 @@ class ModalManager { } createDialog(Element, ...rest) { - return this.createDialogAsync((cb) => {cb(Element);}, ...rest); + return this.createDialogAsync(new Promise(resolve => resolve(Element)), ...rest); } createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { @@ -133,9 +154,8 @@ class ModalManager { * require([''], cb); * } * - * @param {Function} loader a function which takes a 'callback' argument, - * which it should call with a React component which will be displayed as - * the modal view. + * @param {Promise} prom a promise which resolves with a React component + * which will be displayed as the modal view. * * @param {Object} props properties to pass to the displayed * component. (We will also pass an 'onFinished' property.) @@ -147,7 +167,7 @@ class ModalManager { * Also, when closed, all modals will be removed * from the stack. */ - createDialogAsync(loader, props, className, isPriorityModal) { + createDialogAsync(prom, props, className, isPriorityModal) { const self = this; const modal = {}; @@ -178,7 +198,7 @@ class ModalManager { // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // property set here so you can't close the dialog from a button click! modal.elem = ( - ); modal.onFinished = props ? props.onFinished : null; diff --git a/src/Registration.js b/src/Registration.js index 070178fecb..f86c9cc618 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -45,7 +45,7 @@ export async function startAnyRegistrationFlow(options) { // caution though. const hasIlagFlow = flows.some((flow) => { return flow.stages.every((stage) => { - return ['m.login.dummy', 'm.login.recaptcha'].includes(stage); + return ['m.login.dummy', 'm.login.recaptcha', 'm.login.terms'].includes(stage); }); }); diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 3a8e77293b..8a34ba7ab1 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -24,6 +24,8 @@ import sdk from './index'; import {_t, _td} from './languageHandler'; import Modal from './Modal'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; +import {MATRIXTO_URL_PATTERN} from "./linkify-matrix"; +import * as querystring from "querystring"; class Command { @@ -153,11 +155,24 @@ export const CommandMap = { description: _td('Joins room with given alias'), runFn: function(roomId, args) { if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - let roomAlias = matches[1]; - if (roomAlias[0] !== '#') return reject(this.getUsage()); + // Note: we support 2 versions of this command. The first is + // the public-facing one for most users and the other is a + // power-user edition where someone may join via permalink or + // room ID with optional servers. Practically, this results + // in the following variations: + // /join #example:example.org + // /join !example:example.org + // /join !example:example.org altserver.com elsewhere.ca + // /join https://matrix.to/#/!example:example.org?via=altserver.com + // The command also supports event permalinks transparently: + // /join https://matrix.to/#/!example:example.org/$something:example.org + // /join https://matrix.to/#/!example:example.org/$something:example.org?via=altserver.com + const params = args.split(' '); + if (params.length < 1) return reject(this.getUsage()); + const matrixToMatches = params[0].match(MATRIXTO_URL_PATTERN); + if (params[0][0] === '#') { + let roomAlias = params[0]; if (!roomAlias.includes(':')) { roomAlias += ':' + MatrixClientPeg.get().getDomain(); } @@ -167,7 +182,65 @@ export const CommandMap = { room_alias: roomAlias, auto_join: true, }); + return success(); + } else if (params[0][0] === '!') { + const roomId = params[0]; + const viaServers = params.splice(0); + dis.dispatch({ + action: 'view_room', + room_id: roomId, + opts: { + // These are passed down to the js-sdk's /join call + server_name: viaServers, + }, + auto_join: true, + }); + return success(); + } else if (matrixToMatches) { + let entity = matrixToMatches[1]; + let eventId = null; + let viaServers = []; + + if (entity[0] !== '!' && entity[0] !== '#') return reject(this.getUsage()); + + if (entity.indexOf('?') !== -1) { + const parts = entity.split('?'); + entity = parts[0]; + + const parsed = querystring.parse(parts[1]); + viaServers = parsed["via"]; + if (typeof viaServers === 'string') viaServers = [viaServers]; + } + + // We quietly support event ID permalinks too + if (entity.indexOf('/$') !== -1) { + const parts = entity.split("/$"); + entity = parts[0]; + eventId = `$${parts[1]}`; + } + + const dispatch = { + action: 'view_room', + auto_join: true, + }; + + if (entity[0] === '!') dispatch["room_id"] = entity; + else dispatch["room_alias"] = entity; + + if (eventId) { + dispatch["event_id"] = eventId; + dispatch["highlighted"] = true; + } + + if (viaServers) { + dispatch["opts"] = { + // These are passed down to the js-sdk's /join call + server_name: viaServers, + }; + } + + dis.dispatch(dispatch); return success(); } } @@ -492,6 +565,7 @@ export const CommandMap = { const aliases = { j: "join", newballsplease: "discardsession", + goto: "join", // because it handles event permalinks magically }; diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js new file mode 100644 index 0000000000..2f43d18072 --- /dev/null +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -0,0 +1,460 @@ +/* +Copyright 2018 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. +*/ + +import React from 'react'; +import sdk from '../../../../index'; +import MatrixClientPeg from '../../../../MatrixClientPeg'; + +import FileSaver from 'file-saver'; + +import { _t, _td } from '../../../../languageHandler'; + +const PHASE_PASSPHRASE = 0; +const PHASE_PASSPHRASE_CONFIRM = 1; +const PHASE_SHOWKEY = 2; +const PHASE_KEEPITSAFE = 3; +const PHASE_BACKINGUP = 4; +const PHASE_DONE = 5; +const PHASE_OPTOUT_CONFIRM = 6; + +// XXX: copied from ShareDialog: factor out into utils +function selectText(target) { + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +} + +/** + * Walks the user through the process of creating an e2e key backup + * on the server. + */ +export default React.createClass({ + getInitialState: function() { + return { + phase: PHASE_PASSPHRASE, + passPhrase: '', + passPhraseConfirm: '', + copied: false, + downloaded: false, + }; + }, + + componentWillMount: function() { + this._recoveryKeyNode = null; + this._keyBackupInfo = null; + }, + + _collectRecoveryKeyNode: function(n) { + this._recoveryKeyNode = n; + }, + + _onCopyClick: function() { + selectText(this._recoveryKeyNode); + const successful = document.execCommand('copy'); + if (successful) { + this.setState({ + copied: true, + phase: PHASE_KEEPITSAFE, + }); + } + }, + + _onDownloadClick: function() { + const blob = new Blob([this._keyBackupInfo.recovery_key], { + type: 'text/plain;charset=us-ascii', + }); + FileSaver.saveAs(blob, 'recovery-key.txt'); + + this.setState({ + downloaded: true, + phase: PHASE_KEEPITSAFE, + }); + }, + + _createBackup: function() { + this.setState({ + phase: PHASE_BACKINGUP, + error: null, + }); + this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ).then((info) => { + return MatrixClientPeg.get().backupAllGroupSessions(info.version); + }).then(() => { + this.setState({ + phase: PHASE_DONE, + }); + }).catch(e => { + console.log("Error creating key backup", e); + this.setState({ + error: e, + }); + }); + }, + + _onCancel: function() { + this.props.onFinished(false); + }, + + _onDone: function() { + this.props.onFinished(true); + }, + + _onOptOutClick: function() { + this.setState({phase: PHASE_OPTOUT_CONFIRM}); + }, + + _onSetUpClick: function() { + this.setState({phase: PHASE_PASSPHRASE}); + }, + + _onSkipPassPhraseClick: async function() { + this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(); + this.setState({ + copied: false, + phase: PHASE_SHOWKEY, + }); + }, + + _onPassPhraseNextClick: function() { + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + }, + + _onPassPhraseKeyPress: function(e) { + if (e.key === 'Enter' && this._passPhraseIsValid()) { + this._onPassPhraseNextClick(); + } + }, + + _onPassPhraseConfirmNextClick: async function() { + this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); + this.setState({ + copied: false, + phase: PHASE_SHOWKEY, + }); + }, + + _onPassPhraseConfirmKeyPress: function(e) { + if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) { + this._onPassPhraseConfirmNextClick(); + } + }, + + _onSetAgainClick: function() { + this.setState({ + passPhrase: '', + passPhraseConfirm: '', + phase: PHASE_PASSPHRASE, + }); + }, + + _onKeepItSafeGotItClick: function() { + this.setState({ + phase: PHASE_SHOWKEY, + }); + }, + + _onPassPhraseChange: function(e) { + this.setState({ + passPhrase: e.target.value, + }); + }, + + _onPassPhraseConfirmChange: function(e) { + this.setState({ + passPhraseConfirm: e.target.value, + }); + }, + + _passPhraseIsValid: function() { + return this.state.passPhrase !== ''; + }, + + _renderPhasePassPhrase: function() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return
+

{_t("Secure your encrypted message history with a Recovery Passphrase.")}

+

{_t("You'll need it if you log out or lose access to this device.")}

+ +
+ +
+ + + +

{_t( + "If you don't want encrypted message history to be availble on other devices, "+ + ".", + {}, + { + button: sub => + {sub} + , + }, + )}

+

{_t( + "Or, if you don't want to create a Recovery Passphrase, skip this step and "+ + ".", + {}, + { + button: sub => + {sub} + , + }, + )}

+
; + }, + + _renderPhasePassPhraseConfirm: function() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + let passPhraseMatch = null; + if (this.state.passPhraseConfirm.length > 0) { + let matchText; + if (this.state.passPhraseConfirm === this.state.passPhrase) { + matchText = _t("That matches!"); + } else { + matchText = _t("That doesn't match."); + } + passPhraseMatch =
+
{matchText}
+
+ + {_t("Go back to set it again.")} + +
+
; + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Type in your Recovery Passphrase to confirm you remember it. " + + "If it helps, add it to your password manager or store it " + + "somewhere safe.", + )}

+
+ {passPhraseMatch} +
+ +
+
+ +
; + }, + + _renderPhaseShowKey: function() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t("Make a copy of this Recovery Key and keep it safe.")}

+

{_t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.")}

+

+

{_t("Your Recovery Key")}
+
+ + { + // FIXME REDESIGN: buttons should be adjacent but insufficient room in current design + } +

+ +
+
+ {this._keyBackupInfo.recovery_key} +
+

+
+ +
; + }, + + _renderPhaseKeepItSafe: function() { + let introText; + if (this.state.copied) { + introText = _t( + "Your Recovery Key has been copied to your clipboard, paste it to:", + {}, {b: s => {s}}, + ); + } else if (this.state.downloaded) { + introText = _t( + "Your Recovery Key is in your Downloads folder.", + {}, {b: s => {s}}, + ); + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+ {introText} +
    +
  • {_t("Print it and store it somewhere safe", {}, {b: s => {s}})}
  • +
  • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
  • +
  • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
  • +
+ +
; + }, + + _renderBusyPhase: function(text) { + const Spinner = sdk.getComponent('views.elements.Spinner'); + return
+

{_t(text)}

+ +
; + }, + + _renderPhaseDone: function() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t("Backup created")}

+

{_t("Your encryption keys are now being backed up to your Homeserver.")}

+ +
; + }, + + _renderPhaseOptOutConfirm: function() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+ {_t( + "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.", + )} + + + +
; + }, + + _titleForPhase: function(phase) { + switch (phase) { + case PHASE_PASSPHRASE: + return _t('Create a Recovery Passphrase'); + case PHASE_PASSPHRASE_CONFIRM: + return _t('Confirm Recovery Passphrase'); + case PHASE_OPTOUT_CONFIRM: + return _t('Warning!'); + case PHASE_SHOWKEY: + return _t('Recovery Key'); + case PHASE_KEEPITSAFE: + return _t('Keep it safe'); + case PHASE_BACKINGUP: + return _t('Backing up...'); + default: + return _t("Create Key Backup"); + } + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + let content; + if (this.state.error) { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + content =
+

{_t("Unable to create key backup")}

+
+ +
+
; + } else { + switch (this.state.phase) { + case PHASE_PASSPHRASE: + content = this._renderPhasePassPhrase(); + break; + case PHASE_PASSPHRASE_CONFIRM: + content = this._renderPhasePassPhraseConfirm(); + break; + case PHASE_SHOWKEY: + content = this._renderPhaseShowKey(); + break; + case PHASE_KEEPITSAFE: + content = this._renderPhaseKeepItSafe(); + break; + case PHASE_BACKINGUP: + content = this._renderBusyPhase(_td("Backing up...")); + break; + case PHASE_DONE: + content = this._renderPhaseDone(); + break; + case PHASE_OPTOUT_CONFIRM: + content = this._renderPhaseOptOutConfirm(); + break; + } + } + + return ( + +
+ {content} +
+
+ ); + }, +}); diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js index 89053b35c7..01aabf6115 100644 --- a/src/components/structures/HomePage.js +++ b/src/components/structures/HomePage.js @@ -23,6 +23,8 @@ import request from 'browser-request'; import { _t } from '../../languageHandler'; import sanitizeHtml from 'sanitize-html'; import sdk from '../../index'; +import { MatrixClient } from 'matrix-js-sdk'; +import dis from '../../dispatcher'; class HomePage extends React.Component { static displayName = 'HomePage'; @@ -37,6 +39,10 @@ class HomePage extends React.Component { homePageUrl: PropTypes.string, }; + static contextTypes = { + matrixClient: PropTypes.instanceOf(MatrixClient), + }; + state = { iframeSrc: '', page: '', @@ -85,10 +91,47 @@ class HomePage extends React.Component { this._unmounted = true; } + onLoginClick() { + dis.dispatch({ action: 'start_login' }); + } + + onRegisterClick() { + dis.dispatch({ action: 'start_registration' }); + } + render() { + let guestWarning = ""; + if (this.context.matrixClient.isGuest()) { + guestWarning = ( +
+ +
+
+ { _t("You are currently using Riot anonymously as a guest.") } +
+
+ { _t( + 'If you would like to create a Matrix account you can register now.', + {}, + { 'a': (sub) => { sub } }, + ) } +
+
+ { _t( + 'If you already have a Matrix account you can log in instead.', + {}, + { 'a': (sub) => { sub } }, + ) } +
+
+
+ ); + } + if (this.state.iframeSrc) { return (
+ { guestWarning }