Merge remote-tracking branch 'upstream/develop' into hs/upload-limits

This commit is contained in:
Will Hunt 2018-12-01 16:24:43 +00:00
commit 2b077b4f5d
142 changed files with 5235 additions and 671 deletions

View file

@ -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"]
}

View file

@ -7,11 +7,8 @@ src/component-index.js
src/components/structures/BottomLeftMenu.js
src/components/structures/CompatibilityPage.js
src/components/structures/CreateRoom.js
src/components/structures/HomePage.js
src/components/structures/LeftPanel.js
src/components/structures/LoggedInView.js
src/components/structures/login/ForgotPassword.js
src/components/structures/LoginBox.js
src/components/structures/MessagePanel.js
src/components/structures/NotificationPanel.js
src/components/structures/RoomDirectory.js
@ -22,22 +19,17 @@ src/components/structures/SearchBox.js
src/components/structures/TimelinePanel.js
src/components/structures/UploadBar.js
src/components/structures/UserSettings.js
src/components/structures/ViewSource.js
src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/MemberAvatar.js
src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/ChangelogDialog.js
src/components/views/dialogs/DeactivateAccountDialog.js
src/components/views/dialogs/SetPasswordDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js
src/components/views/directory/NetworkDropdown.js
src/components/views/elements/AddressSelector.js
src/components/views/elements/DeviceVerifyButtons.js
src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/ImageView.js
src/components/views/elements/InlineSpinner.js
src/components/views/elements/MemberEventListSummary.js
src/components/views/elements/Spinner.js
src/components/views/elements/TintableSvg.js
src/components/views/elements/UserSelector.js
src/components/views/globals/MatrixToolbar.js
@ -90,7 +82,6 @@ src/MatrixClientPeg.js
src/Modal.js
src/notifications/ContentRules.js
src/notifications/PushRuleVectorState.js
src/notifications/StandardActions.js
src/notifications/VectorPushRulesDefinitions.js
src/Notifier.js
src/PlatformPeg.js
@ -111,7 +102,6 @@ src/utils/MultiInviter.js
src/utils/Receipt.js
src/VectorConferenceHandler.js
src/Velociraptor.js
src/VelocityBounce.js
src/WhoIsTyping.js
src/wrappers/withMatrixClient.js
test/components/structures/login/Registration-test.js

View file

@ -27,12 +27,15 @@ npm run build
npm run test
popd
# run end to end tests
git clone https://github.com/matrix-org/matrix-react-end-to-end-tests.git --branch master
pushd matrix-react-end-to-end-tests
ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
./install.sh
./run.sh --travis
popd
if [ "$TRAVIS_BRANCH" = "develop" ]
then
# run end to end tests
git clone https://github.com/matrix-org/matrix-react-end-to-end-tests.git --branch master
pushd matrix-react-end-to-end-tests
ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
./install.sh
./run.sh --travis
popd
fi

View file

@ -1,3 +1,135 @@
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)
* Fix autoreplacement of ascii emoji
[\#2258](https://github.com/matrix-org/matrix-react-sdk/pull/2258)
Changes in [0.14.2-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.2-rc.1) (2018-10-24)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.1...v0.14.2-rc.1)
* Update from Weblate.
[\#2244](https://github.com/matrix-org/matrix-react-sdk/pull/2244)
* Show the group member list again
[\#2223](https://github.com/matrix-org/matrix-react-sdk/pull/2223)
* lint: make colorScheme camel case
[\#2237](https://github.com/matrix-org/matrix-react-sdk/pull/2237)
* Change leave room button text, OK -> Leave
[\#2236](https://github.com/matrix-org/matrix-react-sdk/pull/2236)
* Move all dialog buttons to the right and fix their order
[\#2231](https://github.com/matrix-org/matrix-react-sdk/pull/2231)
* Add a bit of text to explain the purpose of the RoomPreviewSpinner
[\#2225](https://github.com/matrix-org/matrix-react-sdk/pull/2225)
* Move the login box from the left sidebar to where the composer is
[\#2219](https://github.com/matrix-org/matrix-react-sdk/pull/2219)
* Fix an error where React doesn't like value=null on a select
[\#2230](https://github.com/matrix-org/matrix-react-sdk/pull/2230)
* add missing sticker translation
[\#2216](https://github.com/matrix-org/matrix-react-sdk/pull/2216)
* Support m.login.terms during registration
[\#2221](https://github.com/matrix-org/matrix-react-sdk/pull/2221)
* Don't show the invite nag bar when peeking
[\#2220](https://github.com/matrix-org/matrix-react-sdk/pull/2220)
* Apply the user's tint once the MatrixClientPeg is moderately ready
[\#2214](https://github.com/matrix-org/matrix-react-sdk/pull/2214)
* Make rageshake use less memory
[\#2217](https://github.com/matrix-org/matrix-react-sdk/pull/2217)
* Fix autocomplete
[\#2212](https://github.com/matrix-org/matrix-react-sdk/pull/2212)
* Explain feature states in a lot more detail
[\#2211](https://github.com/matrix-org/matrix-react-sdk/pull/2211)
* Fix various lint errors
[\#2213](https://github.com/matrix-org/matrix-react-sdk/pull/2213)
Changes in [0.14.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.1) (2018-10-19)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.0...v0.14.1)
* Apply the user's tint once the MatrixClientPeg is moderately ready
[\#2214](https://github.com/matrix-org/matrix-react-sdk/pull/2214)
Changes in [0.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.0) (2018-10-16)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.0-rc.1...v0.14.0)

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.14.0",
"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",
@ -100,7 +101,7 @@
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^6.1.2",
"babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
@ -114,9 +115,9 @@
"babel-preset-react": "^6.24.1",
"chokidar": "^1.6.1",
"concurrently": "^4.0.1",
"eslint": "^3.13.1",
"eslint": "^5.8.0",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.1.2",
"eslint-plugin-babel": "^5.2.1",
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^7.7.0",
"estree-walker": "^0.5.0",

View file

@ -170,8 +170,7 @@ textarea {
font-weight: 300;
font-size: 15px;
position: relative;
padding-left: 58px;
padding-bottom: 36px;
padding: 0 58px 36px;
width: 60%;
max-width: 704px;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2);
@ -216,12 +215,16 @@ textarea {
}
.mx_Dialog_content {
margin: 24px 58px 68px 0;
margin: 24px 0 68px;
font-size: 14px;
color: $primary-fg-color;
word-wrap: break-word;
}
.mx_Dialog_buttons {
text-align: right;
}
.mx_Dialog button, .mx_Dialog input[type="submit"] {
@mixin mx_DialogButton;
margin-left: 0px;
@ -259,6 +262,11 @@ textarea {
opacity: 0.7;
}
.mx_linkButton {
cursor: pointer;
color: $accent-color;
}
.mx_Dialog_title {
min-height: 16px;
padding-top: 40px;

View file

@ -33,18 +33,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/_QuestionDialog.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";
@ -108,6 +111,7 @@
@import "./views/rooms/_TopUnreadMessagesBar.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";

View file

@ -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;
}

View file

@ -19,6 +19,7 @@ limitations under the License.
height: unset !important;
padding-top: 13px !important;
padding-bottom: 14px !important;
order: 4;
}
.mx_LoginBox_loginButton_wrapper {

View file

@ -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;

View file

@ -25,6 +25,10 @@ limitations under the License.
line-height: 16px;
}
.mx_TagTileContextMenu_item object {
pointer-events: none;
}
.mx_TagTileContextMenu_item_icon {
padding-right: 8px;

View file

@ -14,14 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ChatInviteDialog {
/* XXX: padding-left is on mx_Dialog but padding-right has subsequently
* been added on other dialogs. Surely all our dialogs should have consistent
* right hand padding?
*/
padding-right: 58px;
}
/* Using a textarea for this element, to circumvent autofill */
.mx_ChatInviteDialog_input,
.mx_ChatInviteDialog_input:focus

View file

@ -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;
}

View file

@ -24,4 +24,8 @@ limitations under the License.
@mixin mx_DialogButton;
background-color: $primary-bg-color;
color: $accent-color;
}
}
.mx_EncryptedEventDialog button {
margin-top: 0px;
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2017 New Vector 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.
@ -13,6 +13,7 @@ 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_QuestionDialog {
padding-right: 58px;
.mx_RestoreKeyBackupDialog_keyStatus {
height: 30px;
}

View file

@ -14,11 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ShareDialog {
// this is to center the content
padding-right: 58px;
}
.mx_ShareDialog hr {
margin-top: 25px;
margin-bottom: 25px;

View file

@ -20,9 +20,6 @@ limitations under the License.
// is a pain in the ass. plus might as well make the dialog big given how
// important it is.
height: 100%;
// position the gemini scrollbar nicely
padding-right: 58px;
}
.mx_UnknownDeviceDialog {
@ -51,4 +48,4 @@ limitations under the License.
.mx_UnknownDeviceDialog .mx_UnknownDeviceDialog_deviceList li {
height: 40px;
border-bottom: 1px solid $primary-hairline-color;
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2107 Vector Creations Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -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;
}

View file

@ -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;
}

View file

@ -19,7 +19,6 @@ limitations under the License.
}
.mx_MemberDeviceInfo.mx_DeviceVerifyButtons {
padding: 6px 0;
display: flex;
flex-wrap: wrap;
justify-content: space-between;

View file

@ -111,6 +111,3 @@ limitations under the License.
width: 100%;
}
.mx_MemberList_outerWrapper {
height: 0px;
}

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2107 Vector Creations Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -56,3 +56,7 @@ limitations under the License.
.mx_RoomPreviewBar_warningIcon {
padding: 12px;
}
.mx_RoomPreviewBar_spinnerIntro {
margin-top: 50px;
}

View file

@ -28,6 +28,13 @@ limitations under the License.
margin-right: 8px;
}
.mx_RoomSettings_devtoolsButton {
@mixin mx_DialogButton;
position: relative;
padding: 4px 1.5em;
margin-top: 8px;
}
.mx_RoomSettings_upgradeButton,
.mx_RoomSettings_leaveButton:hover,
.mx_RoomSettings_unbanButton:hover {

View file

@ -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 (
<div>
<button onClick={this.props.onSubmit}>{ _t("Sign in with CAS") }</button>
</div>
);
},
});
.mx_KeyBackupPanel_deviceName {
font-style: italic;
}

View file

@ -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;
@ -143,6 +144,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;

View file

@ -18,6 +18,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;
@ -148,6 +149,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;

View file

@ -22,7 +22,6 @@ import _clamp from 'lodash/clamp';
type MessageFormat = 'rich' | 'markdown';
class HistoryItem {
// We store history items in their native format to ensure history is accurate
// and then convert them if our RTE has subsequently changed format.
value: Value;

View file

@ -78,7 +78,6 @@ class MemberEntity extends Entity {
}
class UserEntity extends Entity {
constructor(model, showInviteButton, inviteFn) {
super(model);
this.showInviteButton = Boolean(showInviteButton);

View file

@ -64,7 +64,7 @@ export function containsEmoji(str) {
* because we want to include emoji shortnames in title text
*/
function unicodeToImage(str) {
let replaceWith, unicode, alt, short, fname;
let replaceWith; let unicode; let alt; let short; let fname;
const mappedUnicode = emojione.mapUnicodeToShort();
str = str.replace(emojione.regUnicode, function(unicodeChar) {

View file

@ -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);
}
}

View file

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

View file

@ -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 <Component {...otherProps} />;
} else if (this.state.error) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <BaseDialog onFinished={this.props.onFinished}
title={_t("Error")}
>
{_t("Unable to load! Check your network connectivity and try again.")}
<DialogButtons primaryButton={_t("Dismiss")}
onPrimaryButtonClick={this._onWrapperCancelClick}
hasCancel={false}
/>
</BaseDialog>;
} 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(['<module>'], 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 = (
<AsyncWrapper key={modalCount} loader={loader} {...props}
<AsyncWrapper key={modalCount} prom={prom} {...props}
onFinished={closeDialog} />
);
modal.onFinished = props ? props.onFinished : null;

View file

@ -25,7 +25,6 @@ import { _t } from './languageHandler';
* API on the homeserver in question with the new password.
*/
class PasswordReset {
/**
* Configure the endpoints for password resetting.
* @param {string} homeserverUrl The URL to the HS which has the account to reset.

View file

@ -23,7 +23,6 @@ const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
const PRESENCE_STATES = ["online", "offline", "unavailable"];
class Presence {
/**
* Start listening the user activity to evaluate his presence state.
* Any state change will be sent to the Home Server.

View file

@ -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);
});
});

View file

@ -32,7 +32,6 @@ export function getDisplayAliasForRoom(room) {
* return the other one. Otherwise, return null.
*/
export function getOnlyOtherMember(room, myUserId) {
if (room.currentState.getJoinedMemberCount() === 2) {
return room.getJoinedMembers().filter(function(m) {
return m.userId !== myUserId;
@ -103,7 +102,7 @@ export function guessAndSetDMRoom(room, isDirect) {
let newTarget;
if (isDirect) {
const guessedUserId = guessDMRoomTargetId(
room, MatrixClientPeg.get().getUserId()
room, MatrixClientPeg.get().getUserId(),
);
newTarget = guessedUserId;
} else {

View file

@ -22,7 +22,6 @@ const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require('./MatrixClientPeg');
class ScalarAuthClient {
constructor() {
this.scalarToken = null;
}

View file

@ -24,7 +24,6 @@ const DEFAULTS = {
};
class SdkConfig {
static get() {
return global.mxReactSdkConfig || {};
}

View file

@ -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
};

View file

@ -25,7 +25,6 @@ const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
* with the app (but at a much lower frequency than mouse move events)
*/
class UserActivity {
/**
* Start listening to user activity
*/

View file

@ -87,7 +87,7 @@ export default {
device_display_name: address,
lang: navigator.language,
data: data,
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
});
},
};

View file

@ -3,8 +3,8 @@ const Velocity = require('velocity-vector');
// courtesy of https://github.com/julianshapiro/velocity/issues/283
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
function bounce( p ) {
let pow2,
bounce = 4;
let pow2;
let bounce = 4;
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
// just sets pow2

View file

@ -62,6 +62,35 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
};
}
/**
* @typedef RoomAccountDataAction
* @type {Object}
* @property {string} action 'MatrixActions.Room.accountData'.
* @property {MatrixEvent} event the MatrixEvent that triggered the dispatch.
* @property {string} event_type the type of the MatrixEvent, e.g. "m.direct".
* @property {Object} event_content the content of the MatrixEvent.
* @property {Room} room the room where the account data was changed.
*/
/**
* Create a MatrixActions.Room.accountData action that represents a MatrixClient `Room.accountData`
* matrix event.
*
* @param {MatrixClient} matrixClient the matrix client.
* @param {MatrixEvent} accountDataEvent the account data event.
* @param {Room} room the room where account data was changed
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
*/
function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
return {
action: 'MatrixActions.Room.accountData',
event: accountDataEvent,
event_type: accountDataEvent.getType(),
event_content: accountDataEvent.getContent(),
room: room,
};
}
/**
* @typedef RoomAction
* @type {Object}
@ -201,6 +230,7 @@ export default {
start(matrixClient) {
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);

View file

@ -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 <div>
<p>{_t("Secure your encrypted message history with a Recovery Passphrase.")}</p>
<p>{_t("You'll need it if you log out or lose access to this device.")}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<input type="password"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
/>
</div>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNextClick}
hasCancel={false}
disabled={!this._passPhraseIsValid()}
/>
<p>{_t(
"If you don't want encrypted message history to be availble on other devices, "+
"<button>opt out</button>.",
{},
{
button: sub => <AccessibleButton
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>;
},
_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 = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
<div>{matchText}</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{_t("Go back to set it again.")}
</AccessibleButton>
</div>
</div>;
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_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.",
)}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
{passPhraseMatch}
<div>
<input type="password"
onChange={this._onPassPhraseConfirmChange}
onKeyPress={this._onPassPhraseConfirmKeyPress}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")}
autoFocus={true}
/>
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/>
</div>;
},
_renderPhaseShowKey: function() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p>
<p>{_t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.")}</p>
<p className="mx_CreateKeyBackupDialog_primaryContainer">
<div>{_t("Your Recovery Key")}</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
{
// FIXME REDESIGN: buttons should be adjacent but insufficient room in current design
}
<br /><br />
<button onClick={this._onDownloadClick}>
{_t("Download")}
</button>
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
</div>
</p>
<br />
<DialogButtons primaryButton={_t("I've made a copy")}
onPrimaryButtonClick={this._createBackup}
hasCancel={false}
disabled={!this.state.copied && !this.state.downloaded}
/>
</div>;
},
_renderPhaseKeepItSafe: function() {
let introText;
if (this.state.copied) {
introText = _t(
"Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your Recovery Key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{introText}
<ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
</ul>
<DialogButtons primaryButton={_t("Got it")}
onPrimaryButtonClick={this._onKeepItSafeGotItClick}
hasCancel={false}
/>
</div>;
},
_renderBusyPhase: function(text) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<p>{_t(text)}</p>
<Spinner />
</div>;
},
_renderPhaseDone: function() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t("Backup created")}</p>
<p>{_t("Your encryption keys are now being backed up to your Homeserver.")}</p>
<DialogButtons primaryButton={_t('Close')}
onPrimaryButtonClick={this._onDone}
hasCancel={false}
/>
</div>;
},
_renderPhaseOptOutConfirm: function() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{_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.",
)}
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
onPrimaryButtonClick={this._onSetUpClick}
hasCancel={false}
>
<button onClick={this._onCancel}>I understand, continue without</button>
</DialogButtons>
</div>;
},
_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 = <div>
<p>{_t("Unable to create key backup")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._createBackup}
hasCancel={true}
onCancel={this._onCancel}
/>
</div>
</div>;
} 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 (
<BaseDialog className='mx_CreateKeyBackupDialog'
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
hasCancel={[PHASE_DONE].includes(this.state.phase)}
>
<div>
{content}
</div>
</BaseDialog>
);
},
});

View file

@ -85,7 +85,7 @@ export default class Autocompleter {
provider
.getCompletions(query, selection, force)
.timeout(PROVIDER_COMPLETION_TIMEOUT)
.reflect()
.reflect(),
),
);

View file

@ -61,7 +61,7 @@ export default class CommunityProvider extends AutocompleteProvider {
if (command) {
const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join');
const groups = (await Promise.all(joinedGroups.map(async ({groupId}) => {
const groups = (await Promise.all(joinedGroups.map(async({groupId}) => {
try {
return FlairStore.getGroupProfileCached(cli, groupId);
} catch (e) { // if FlairStore failed, fall back to just groupId

View file

@ -26,7 +26,6 @@ import { Block } from 'slate';
*/
class PlainWithPillsSerializer {
/*
* @param {String} options.pillFormat - either 'md', 'plain', 'id'
*/

View file

@ -33,12 +33,12 @@ module.exports = React.createClass({
},
getInitialState: function() {
return({
directoryHover : false,
roomsHover : false,
return ({
directoryHover: false,
roomsHover: false,
homeHover: false,
peopleHover : false,
settingsHover : false,
peopleHover: false,
settingsHover: false,
});
},
@ -145,7 +145,7 @@ module.exports = React.createClass({
// Get the label/tooltip to show
getLabel: function(label, show) {
if (show) {
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
return <RoomTooltip className="mx_BottomLeftMenu_tooltip" label={label} />;
}
},

View file

@ -16,18 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
import { _t } from '../../languageHandler';
module.exports = React.createClass({
displayName: 'CompatibilityPage',
propTypes: {
onAccept: React.PropTypes.func
onAccept: React.PropTypes.func,
},
getDefaultProps: function() {
return {
onAccept: function() {} // NOP
onAccept: function() {}, // NOP
};
},
@ -36,7 +36,6 @@ module.exports = React.createClass({
},
render: function() {
return (
<div className="mx_CompatibilityPage">
<div className="mx_CompatibilityPage_box">
@ -69,5 +68,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -36,10 +36,10 @@ module.exports = React.createClass({
},
phases: {
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
CREATING: "CREATING", // We're sending the request.
CREATED: "CREATED", // We successfully created the room.
ERROR: "ERROR", // There was an error while trying to create room.
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
CREATING: "CREATING", // We're sending the request.
CREATED: "CREATED", // We successfully created the room.
ERROR: "ERROR", // There was an error while trying to create room.
},
getDefaultProps: function() {

View file

@ -746,14 +746,38 @@ export default React.createClass({
});
},
_leaveGroupWarnings: function() {
const warnings = [];
if (this.state.isUserPrivileged) {
warnings.push((
<span className="warning">
{ " " /* Whitespace, otherwise the sentences get smashed together */ }
{ _t("You are an administrator of this community. You will not be " +
"able to rejoin without an invite from another administrator.") }
</span>
));
}
return warnings;
},
_onLeaveClick: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const warnings = this._leaveGroupWarnings();
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
title: _t("Leave Community"),
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
description: (
<span>
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
{ warnings }
</span>
),
button: _t("Leave"),
danger: true,
onFinished: async (confirmed) => {
danger: this.state.isUserPrivileged,
onFinished: async(confirmed) => {
if (!confirmed) return;
this.setState({membershipBusy: true});

View file

@ -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: '',
@ -52,15 +58,14 @@ class HomePage extends React.Component {
if (this.props.teamToken && this.props.teamServerUrl) {
this.setState({
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`,
});
}
else {
} else {
// we use request() to inline the homepage into the react component
// so that it can inherit CSS and theming easily rather than mess around
// with iframes and trying to synchronise document.stylesheets.
let src = this.props.homePageUrl || 'home.html';
const src = this.props.homePageUrl || 'home.html';
request(
{ method: "GET", url: src },
@ -77,7 +82,7 @@ class HomePage extends React.Component {
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
this.setState({ page: body });
}
},
);
}
}
@ -86,18 +91,55 @@ class HomePage extends React.Component {
this._unmounted = true;
}
onLoginClick() {
dis.dispatch({ action: 'start_login' });
}
onRegisterClick() {
dis.dispatch({ action: 'start_registration' });
}
render() {
if (this.state.iframeSrc) {
return (
<div className="mx_HomePage">
<iframe src={ this.state.iframeSrc } />
let guestWarning = "";
if (this.context.matrixClient.isGuest()) {
guestWarning = (
<div className="mx_HomePage_guest_warning">
<img src="img/warning.svg" width="24" height="23" />
<div>
<div>
{ _t("You are currently using Riot anonymously as a guest.") }
</div>
<div>
{ _t(
'If you would like to create a Matrix account you can <a>register</a> now.',
{},
{ 'a': (sub) => <a href="#" onClick={this.onRegisterClick}>{ sub }</a> },
) }
</div>
<div>
{ _t(
'If you already have a Matrix account you can <a>log in</a> instead.',
{},
{ 'a': (sub) => <a href="#" onClick={this.onLoginClick}>{ sub }</a> },
) }
</div>
</div>
</div>
);
}
else {
if (this.state.iframeSrc) {
return (
<div className="mx_HomePage">
{ guestWarning }
<iframe src={ this.state.iframeSrc } />
</div>
);
} else {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
return (
<GeminiScrollbarWrapper autoshow={true} className="mx_HomePage">
{ guestWarning }
<div className="mx_HomePage_body" dangerouslySetInnerHTML={{ __html: this.state.page }}>
</div>
</GeminiScrollbarWrapper>
@ -106,4 +148,4 @@ class HomePage extends React.Component {
}
}
module.exports = HomePage;
module.exports = HomePage;

View file

@ -68,6 +68,11 @@ export default React.createClass({
// If true, poll to see if the auth flow has been completed
// out-of-band
poll: PropTypes.bool,
// If true, components will be told that the 'Continue' button
// is managed by some other party and should not be managed by
// the component itself.
continueIsManaged: PropTypes.bool,
},
getInitialState: function() {
@ -128,6 +133,12 @@ export default React.createClass({
}
},
tryContinue: function() {
if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) {
this.refs.stageComponent.tryContinue();
}
},
_authStateUpdated: function(stageType, stageState) {
const oldStage = this.state.authStage;
this.setState({
@ -192,6 +203,7 @@ export default React.createClass({
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
makeRegistrationUrl={this.props.makeRegistrationUrl}
showContinue={!this.props.continueIsManaged}
/>
);
},

View file

@ -28,7 +28,7 @@ import VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
var LeftPanel = React.createClass({
const LeftPanel = React.createClass({
displayName: 'LeftPanel',
// NB. If you add props, don't forget to update
@ -181,14 +181,8 @@ var LeftPanel = React.createClass({
const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
const CallPreview = sdk.getComponent('voip.CallPreview');
let topBox;
if (this.context.matrixClient.isGuest()) {
const LoginBox = sdk.getComponent('structures.LoginBox');
topBox = <LoginBox collapsed={ this.props.collapsed }/>;
} else {
const SearchBox = sdk.getComponent('structures.SearchBox');
topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
}
const SearchBox = sdk.getComponent('structures.SearchBox');
const topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
const classes = classNames(
"mx_LeftPanel",
@ -220,11 +214,11 @@ var LeftPanel = React.createClass({
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />
<BottomLeftMenu collapsed={this.props.collapsed}/>
<BottomLeftMenu collapsed={this.props.collapsed} />
</aside>
</div>
);
}
},
});
module.exports = LeftPanel;

View file

@ -64,6 +64,9 @@ const LoggedInView = React.createClass({
teamToken: PropTypes.string,
// Used by the RoomView to handle joining rooms
viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff.
},
@ -186,13 +189,13 @@ const LoggedInView = React.createClass({
_updateServerNoticeEvents: async function() {
const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return [];
const pinnedEvents = [];
for (const room of roomLists['m.server_notice']) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
@ -204,7 +207,7 @@ const LoggedInView = React.createClass({
serverNoticeEvents: pinnedEvents,
});
},
_onKeyDown: function(ev) {
/*
@ -389,6 +392,7 @@ const LoggedInView = React.createClass({
onRegistered={this.props.onRegistered}
thirdPartyInvite={this.props.thirdPartyInvite}
oobData={this.props.roomOobData}
viaServers={this.props.viaServers}
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations 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.
@ -16,31 +17,15 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
import { _t } from '../../languageHandler';
var sdk = require('../../index')
var dis = require('../../dispatcher');
var rate_limited_func = require('../../ratelimitedfunc');
var AccessibleButton = require('../../components/views/elements/AccessibleButton');
const dis = require('../../dispatcher');
const AccessibleButton = require('../../components/views/elements/AccessibleButton');
module.exports = React.createClass({
displayName: 'LoginBox',
propTypes: {
collapsed: React.PropTypes.bool,
},
onToggleCollapse: function(show) {
if (show) {
dis.dispatch({
action: 'show_left_panel',
});
}
else {
dis.dispatch({
action: 'hide_left_panel',
});
}
},
onLoginClick: function() {
@ -52,42 +37,21 @@ module.exports = React.createClass({
},
render: function() {
var TintableSvg = sdk.getComponent('elements.TintableSvg');
var toggleCollapse;
if (this.props.collapsed) {
toggleCollapse =
<AccessibleButton className="mx_SearchBox_maximise" onClick={ this.onToggleCollapse.bind(this, true) }>
<TintableSvg src="img/maximise.svg" width="10" height="16" alt="Expand panel"/>
const loginButton = (
<div className="mx_LoginBox_loginButton_wrapper">
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
{ _t("Login") }
</AccessibleButton>
}
else {
toggleCollapse =
<AccessibleButton className="mx_SearchBox_minimise" onClick={ this.onToggleCollapse.bind(this, false) }>
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="Collapse panel"/>
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
{ _t("Register") }
</AccessibleButton>
}
var loginButton;
if (!this.props.collapsed) {
loginButton = (
<div className="mx_LoginBox_loginButton_wrapper">
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
{ _t("Login") }
</AccessibleButton>
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
{ _t("Register") }
</AccessibleButton>
</div>
);
}
var self = this;
return (
<div className="mx_SearchBox mx_LoginBox">
{ loginButton }
{ toggleCollapse }
</div>
);
}
return (
<div className="mx_LoginBox">
{ loginButton }
</div>
);
},
});

View file

@ -840,6 +840,7 @@ export default React.createClass({
page_type: PageTypes.RoomView,
thirdPartyInvite: roomInfo.third_party_invite,
roomOobData: roomInfo.oob_data,
viaServers: roomInfo.via_servers,
};
if (roomInfo.room_alias) {
@ -1034,6 +1035,7 @@ export default React.createClass({
{ warnings }
</span>
),
button: _t("Leave"),
onFinished: (shouldLeave) => {
if (shouldLeave) {
const d = MatrixClientPeg.get().leave(roomId);
@ -1373,6 +1375,7 @@ export default React.createClass({
cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req);
});
cli.on("Room", (room) => {
if (MatrixClientPeg.get().isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(
@ -1403,6 +1406,11 @@ export default React.createClass({
break;
}
});
// Fire the tinter right on startup to ensure the default theme is applied
// A later sync can/will correct the tint to be the right value for the user
const colorScheme = SettingsStore.getValue("roomColor");
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
},
/**
@ -1483,9 +1491,21 @@ export default React.createClass({
inviterName: params.inviter_name,
};
// on our URLs there might be a ?via=matrix.org or similar to help
// joins to the room succeed. We'll pass these through as an array
// to other levels. If there's just one ?via= then params.via is a
// single string. If someone does something like ?via=one.com&via=two.com
// then params.via is an array of strings.
let via = [];
if (params.via) {
if (typeof(params.via) === 'string') via = [params.via];
else via = params.via;
}
const payload = {
action: 'view_room',
event_id: eventId,
via_servers: via,
// If an event ID is given in the URL hash, notify RoomViewStore to mark
// it as highlighted, which will propagate to RoomView and highlight the
// associated EventTile.

View file

@ -16,18 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
var MatrixClientPeg = require('../../MatrixClientPeg');
var ContentRepo = require("matrix-js-sdk").ContentRepo;
var Modal = require('../../Modal');
var sdk = require('../../index');
var dis = require('../../dispatcher');
const MatrixClientPeg = require('../../MatrixClientPeg');
const ContentRepo = require("matrix-js-sdk").ContentRepo;
const Modal = require('../../Modal');
const sdk = require('../../index');
const dis = require('../../dispatcher');
var linkify = require('linkifyjs');
var linkifyString = require('linkifyjs/string');
var linkifyMatrix = require('../../linkify-matrix');
var sanitizeHtml = require('sanitize-html');
const linkify = require('linkifyjs');
const linkifyString = require('linkifyjs/string');
const linkifyMatrix = require('../../linkify-matrix');
const sanitizeHtml = require('sanitize-html');
import Promise from 'bluebird';
import { _t } from '../../languageHandler';
@ -46,7 +46,7 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
config: {},
}
};
},
getInitialState: function() {
@ -58,7 +58,7 @@ module.exports = React.createClass({
includeAll: false,
roomServer: null,
filterString: null,
}
};
},
componentWillMount: function() {
@ -134,13 +134,12 @@ module.exports = React.createClass({
opts.include_all_networks = true;
}
if (this.nextBatch) opts.since = this.nextBatch;
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string } ;
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
if (
my_filter_string != this.state.filterString ||
my_server != this.state.roomServer ||
my_next_batch != this.nextBatch)
{
my_next_batch != this.nextBatch) {
// if the filter or server has changed since this request was sent,
// throw away the result (don't even clear the busy flag
// since we must still have a request in flight)
@ -163,8 +162,7 @@ module.exports = React.createClass({
if (
my_filter_string != this.state.filterString ||
my_server != this.state.roomServer ||
my_next_batch != this.nextBatch)
{
my_next_batch != this.nextBatch) {
// as above: we don't care about errors for old
// requests either
return;
@ -177,10 +175,10 @@ module.exports = React.createClass({
this.setState({ loading: false });
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to get public room list', '', ErrorDialog, {
title: _t('Failed to get public room list'),
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
});
});
},
@ -193,13 +191,13 @@ module.exports = React.createClass({
* this needs SPEC-417.
*/
removeFromDirectory: function(room) {
var alias = get_display_alias_for_room(room);
var name = room.name || alias || _t('Unnamed room');
const alias = get_display_alias_for_room(room);
const name = room.name || alias || _t('Unnamed room');
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var desc;
let desc;
if (alias) {
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
} else {
@ -212,9 +210,9 @@ module.exports = React.createClass({
onFinished: (should_delete) => {
if (!should_delete) return;
var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader);
var step = _t('remove %(name)s from the directory.', {name: name});
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader);
let step = _t('remove %(name)s from the directory.', {name: name});
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
if (!alias) return;
@ -229,10 +227,10 @@ module.exports = React.createClass({
console.error("Failed to " + step + ": " + err);
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
title: _t('Error'),
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
});
});
}
},
});
},
@ -347,7 +345,7 @@ module.exports = React.createClass({
},
showRoom: function(room, room_alias) {
var payload = {action: 'view_room'};
const payload = {action: 'view_room'};
if (room) {
// Don't let the user view a room they won't be able to either
// peek or join: fail earlier so they don't have to click back
@ -383,16 +381,16 @@ module.exports = React.createClass({
},
getRows: function() {
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
if (!this.state.publicRooms) return [];
var rooms = this.state.publicRooms;
var rows = [];
var self = this;
var guestRead, guestJoin, perms;
for (var i = 0; i < rooms.length; i++) {
var name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
const rooms = this.state.publicRooms;
const rows = [];
const self = this;
let guestRead; let guestJoin; let perms;
for (let i = 0; i < rooms.length; i++) {
const name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
guestRead = null;
guestJoin = null;
@ -412,7 +410,7 @@ module.exports = React.createClass({
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
}
var topic = rooms[i].topic || '';
let topic = rooms[i].topic || '';
topic = linkifyString(sanitizeHtml(topic));
rows.push(
@ -432,14 +430,14 @@ module.exports = React.createClass({
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
{ perms }
<div className="mx_RoomDirectory_topic"
onClick={ function(e) { e.stopPropagation() } }
dangerouslySetInnerHTML={{ __html: topic }}/>
onClick={ function(e) { e.stopPropagation(); } }
dangerouslySetInnerHTML={{ __html: topic }} />
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(rooms[i]) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ rooms[i].num_joined_members }
</td>
</tr>
</tr>,
);
}
return rows;
@ -524,7 +522,7 @@ module.exports = React.createClass({
onFillRequest={ this.onFillRequest }
stickyBottom={false}
startAtBottom={false}
onResize={function(){}}
onResize={function() {}}
>
{ scrollpanel_content }
</ScrollPanel>;
@ -577,11 +575,11 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list
function get_display_alias_for_room(room) {
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
}

View file

@ -37,7 +37,7 @@ function getUnsentMessages(room) {
return room.getPendingEvents().filter(function(ev) {
return ev.status === Matrix.EventStatus.NOT_SENT;
});
};
}
module.exports = React.createClass({
displayName: 'RoomStatusBar',
@ -66,6 +66,10 @@ module.exports = React.createClass({
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: PropTypes.number,
// true if the room is being peeked at. This affects components that shouldn't
// logically be shown when peeking, such as a prompt to invite people to a room.
isPeeking: PropTypes.bool,
// callback for when the user clicks on the 'resend all' button in the
// 'unsent messages' bar
onResendAllClick: PropTypes.func,
@ -299,7 +303,7 @@ module.exports = React.createClass({
const errorIsMauError = Boolean(
this.state.syncStateData &&
this.state.syncStateData.error &&
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED'
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED',
);
return this.state.syncState === "ERROR" && !errorIsMauError;
},
@ -457,7 +461,7 @@ module.exports = React.createClass({
}
// If you're alone in the room, and have sent a message, suggest to invite someone
if (this.props.sentMessageAndIsAlone) {
if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) {
return (
<div className="mx_RoomStatusBar_isAlone">
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +

View file

@ -89,6 +89,9 @@ module.exports = React.createClass({
// is the RightPanel collapsed?
collapsedRhs: PropTypes.bool,
// Servers the RoomView can use to try and assist joins
viaServers: PropTypes.arrayOf(PropTypes.string),
},
getInitialState: function() {
@ -708,8 +711,8 @@ module.exports = React.createClass({
if (!room) return;
console.log("Tinter.tint from updateTint");
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
const colorScheme = SettingsStore.getValue("roomColor", room.roomId);
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
},
onAccountData: function(event) {
@ -724,10 +727,10 @@ module.exports = React.createClass({
if (room.roomId == this.state.roomId) {
const type = event.getType();
if (type === "org.matrix.room.color_scheme") {
const color_scheme = event.getContent();
const colorScheme = event.getContent();
// XXX: we should validate the event
console.log("Tinter.tint from onRoomAccountData");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
this._updatePreviewUrlVisibility(room);
@ -863,7 +866,7 @@ module.exports = React.createClass({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'join_room',
opts: { inviteSignUrl: signUrl },
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
},
});
@ -905,7 +908,7 @@ module.exports = React.createClass({
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
action: 'join_room',
opts: { inviteSignUrl: signUrl },
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
});
return Promise.resolve();
});
@ -1553,6 +1556,7 @@ module.exports = React.createClass({
canPreview={false} error={this.state.roomLoadError}
roomAlias={roomAlias}
spinner={this.state.joining}
spinnerState="joining"
inviterName={inviterName}
invitedEmail={invitedEmail}
room={this.state.room}
@ -1597,6 +1601,7 @@ module.exports = React.createClass({
inviterName={inviterName}
canPreview={false}
spinner={this.state.joining}
spinnerState="joining"
room={this.state.room}
/>
</div>
@ -1634,6 +1639,7 @@ module.exports = React.createClass({
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
sentMessageAndIsAlone={this.state.isAlone}
hasActiveCall={inCall}
isPeeking={myMembership !== "join"}
onInviteClick={this.onInviteButtonClick}
onStopWarningClick={this.onStopAloneWarningClick}
onScrollToBottomClick={this.jumpToLiveTimeline}
@ -1683,6 +1689,7 @@ module.exports = React.createClass({
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
spinner={this.state.joining}
spinnerState="joining"
inviterName={inviterName}
invitedEmail={invitedEmail}
canPreview={this.state.canPeek}
@ -1705,10 +1712,10 @@ module.exports = React.createClass({
</AuxPanel>
);
let messageComposer, searchInfo;
let messageComposer; let searchInfo;
const canSpeak = (
// joined and not showing search results
myMembership == 'join' && !this.state.searchResults
myMembership === 'join' && !this.state.searchResults
);
if (canSpeak) {
messageComposer =
@ -1723,6 +1730,11 @@ module.exports = React.createClass({
/>;
}
if (MatrixClientPeg.get().isGuest()) {
const LoginBox = sdk.getComponent('structures.LoginBox');
messageComposer = <LoginBox />;
}
// TODO: Why aren't we storing the term/scope/count in this format
// in this.state if this is what RoomHeader desires?
if (this.state.searchResults) {
@ -1734,7 +1746,7 @@ module.exports = React.createClass({
}
if (inCall) {
let zoomButton, voiceMuteButton, videoMuteButton;
let zoomButton; let voiceMuteButton; let videoMuteButton;
if (call.type === "video") {
zoomButton = (

View file

@ -72,7 +72,7 @@ module.exports = React.createClass({
function() {
this.props.onSearch(this.refs.search.value);
},
100
100,
),
onToggleCollapse: function(show) {
@ -80,8 +80,7 @@ module.exports = React.createClass({
dis.dispatch({
action: 'show_left_panel',
});
}
else {
} else {
dis.dispatch({
action: 'hide_left_panel',
});
@ -103,25 +102,24 @@ module.exports = React.createClass({
},
render: function() {
var TintableSvg = sdk.getComponent('elements.TintableSvg');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
var collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0";
const collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0";
var toggleCollapse;
let toggleCollapse;
if (this.props.collapsed) {
toggleCollapse =
<AccessibleButton className="mx_SearchBox_maximise" tabIndex={collapseTabIndex} onClick={ this.onToggleCollapse.bind(this, true) }>
<TintableSvg src="img/maximise.svg" width="10" height="16" alt={ _t("Expand panel") }/>
</AccessibleButton>
}
else {
<TintableSvg src="img/maximise.svg" width="10" height="16" alt={ _t("Expand panel") } />
</AccessibleButton>;
} else {
toggleCollapse =
<AccessibleButton className="mx_SearchBox_minimise" tabIndex={collapseTabIndex} onClick={ this.onToggleCollapse.bind(this, false) }>
<TintableSvg src="img/minimise.svg" width="10" height="16" alt={ _t("Collapse panel") }/>
</AccessibleButton>
<TintableSvg src="img/minimise.svg" width="10" height="16" alt={ _t("Collapse panel") } />
</AccessibleButton>;
}
var searchControls;
let searchControls;
if (!this.props.collapsed) {
searchControls = [
this.state.searchTerm.length > 0 ?
@ -148,16 +146,16 @@ module.exports = React.createClass({
onChange={ this.onChange }
onKeyDown={ this._onKeyDown }
placeholder={ _t('Filter room names') }
/>
/>,
];
}
var self = this;
const self = this;
return (
<div className="mx_SearchBox">
{ searchControls }
{ toggleCollapse }
</div>
);
}
},
});

View file

@ -829,7 +829,7 @@ var TimelinePanel = React.createClass({
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
const pos = this.getReadMarkerPosition();
return this.state.readMarkerEventId !== null && // 1.
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
(pos < 0 || pos === null); // 3., 4.
},

View file

@ -82,6 +82,9 @@ const SIMPLE_SETTINGS = [
{ id: "TagPanel.disableTagPanel" },
{ id: "enableWidgetScreenshots" },
{ id: "RoomSubList.showEmpty" },
{ id: "pinMentionedRooms" },
{ id: "pinUnreadRooms" },
{ id: "showDeveloperTools" },
];
// These settings must be defined in SettingsStore
@ -586,23 +589,21 @@ module.exports = React.createClass({
},
_onExportE2eKeysClicked: function() {
Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => {
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
});
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../async-components/views/dialogs/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
);
},
_onImportE2eKeysClicked: function() {
Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => {
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
});
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../async-components/views/dialogs/ImportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
);
},
_renderGroupSettings: function() {
@ -736,6 +737,16 @@ module.exports = React.createClass({
</div>
);
}
let keyBackupSection;
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
keyBackupSection = <div className="mx_UserSettings_section">
<h3>{ _t("Key Backup") }</h3>
<KeyBackupPanel />
</div>;
}
return (
<div>
<h3>{ _t("Cryptography") }</h3>
@ -751,6 +762,7 @@ module.exports = React.createClass({
<div className="mx_UserSettings_section">
{ CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
</div>
{keyBackupSection}
</div>
);
},
@ -844,7 +856,7 @@ module.exports = React.createClass({
SettingsStore.getLabsFeatures().forEach((featureId) => {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = async (e) => {
const onChange = async(e) => {
const checked = e.target.checked;
if (featureId === "feature_lazyloading") {
const confirmed = await this._onLazyLoadChanging(checked);
@ -1297,7 +1309,7 @@ module.exports = React.createClass({
// If the olmVersion is not defined then either crypto is disabled, or
// we are using a version old version of olm. We assume the former.
let olmVersionString = "<not-enabled>";
if (olmVersion !== undefined) {
if (olmVersion) {
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
}

View file

@ -53,5 +53,5 @@ module.exports = React.createClass({
</SyntaxHighlight>
</div>
);
}
},
});

View file

@ -121,13 +121,12 @@ module.exports = React.createClass({
},
_onExportE2eKeysClicked: function() {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', (cb) => {
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
});
Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password',
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
);
},
onInputChanged: function(stateKey, ev) {

View file

@ -26,6 +26,7 @@ import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import request from 'browser-request';
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -74,6 +75,11 @@ module.exports = React.createClass({
phoneCountry: null,
phoneNumber: "",
currentFlow: "m.login.password",
// .well-known discovery
discoveredHsUrl: "",
discoveredIsUrl: "",
discoveryError: "",
};
},
@ -84,7 +90,10 @@ module.exports = React.createClass({
// letting you do that login type
this._stepRendererMap = {
'm.login.password': this._renderPasswordStep,
'm.login.cas': this._renderCasStep,
// CAS and SSO are the same thing, modulo the url we link to
'm.login.cas': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("cas")),
'm.login.sso': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("sso")),
};
this._initLoginLogic();
@ -102,6 +111,10 @@ module.exports = React.createClass({
},
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
// Prevent people from submitting their password when homeserver
// discovery went wrong
if (this.state.discoveryError) return;
this.setState({
busy: true,
errorText: null,
@ -186,10 +199,6 @@ module.exports = React.createClass({
}).done();
},
onCasLogin: function() {
this._loginLogic.redirectToCas();
},
_onLoginAsGuestClick: function() {
const self = this;
self.setState({
@ -222,6 +231,22 @@ module.exports = React.createClass({
this.setState({ username: username });
},
onUsernameBlur: function(username) {
this.setState({ username: username });
if (username[0] === "@") {
const serverName = username.split(':').slice(1).join(':');
try {
// we have to append 'https://' to make the URL constructor happy
// otherwise we get things like 'protocol: matrix.org, pathname: 8448'
const url = new URL("https://" + serverName);
this._tryWellKnownDiscovery(url.hostname);
} catch (e) {
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
this.setState({discoveryError: _t("Failed to perform homeserver discovery")});
}
}
},
onPhoneCountryChanged: function(phoneCountry) {
this.setState({ phoneCountry: phoneCountry });
},
@ -257,6 +282,122 @@ module.exports = React.createClass({
});
},
_tryWellKnownDiscovery: async function(serverName) {
if (!serverName.trim()) {
// Nothing to discover
this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""});
return;
}
try {
const wellknown = await this._getWellKnownObject(`https://${serverName}/.well-known/matrix/client`);
if (!wellknown["m.homeserver"]) {
console.error("No m.homeserver key in well-known response");
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
return;
}
const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]);
if (!hsUrl) {
console.error("Invalid base_url for m.homeserver");
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
return;
}
console.log("Verifying homeserver URL: " + hsUrl);
const hsVersions = await this._getWellKnownObject(`${hsUrl}/_matrix/client/versions`);
if (!hsVersions["versions"]) {
console.error("Invalid /versions response");
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
return;
}
let isUrl = "";
if (wellknown["m.identity_server"]) {
isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
if (!isUrl) {
console.error("Invalid base_url for m.identity_server");
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
return;
}
console.log("Verifying identity server URL: " + isUrl);
const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`);
if (!isResponse) {
console.error("Invalid /api/v1 response");
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
return;
}
}
this.setState({discoveredHsUrl: hsUrl, discoveredIsUrl: isUrl, discoveryError: ""});
} catch (e) {
console.error(e);
if (e.wkAction) {
if (e.wkAction === "FAIL_ERROR" || e.wkAction === "FAIL_PROMPT") {
// We treat FAIL_ERROR and FAIL_PROMPT the same to avoid having the user
// submit their details to the wrong homeserver. In practice, the custom
// server options will show up to try and guide the user into entering
// the required information.
this.setState({discoveryError: _t("Cannot find homeserver")});
return;
} else if (e.wkAction === "IGNORE") {
// Nothing to discover
this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""});
return;
}
}
throw e;
}
},
_sanitizeWellKnownUrl: function(url) {
if (!url) return false;
const parser = document.createElement('a');
parser.href = url;
if (parser.protocol !== "http:" && parser.protocol !== "https:") return false;
if (!parser.hostname) return false;
const port = parser.port ? `:${parser.port}` : "";
const path = parser.pathname ? parser.pathname : "";
let saferUrl = `${parser.protocol}//${parser.hostname}${port}${path}`;
if (saferUrl.endsWith("/")) saferUrl = saferUrl.substring(0, saferUrl.length - 1);
return saferUrl;
},
_getWellKnownObject: function(url) {
return new Promise(function(resolve, reject) {
request(
{ method: "GET", url: url },
(err, response, body) => {
if (err || response.status < 200 || response.status >= 300) {
let action = "FAIL_ERROR";
if (response.status === 404) {
// We could just resolve with an empty object, but that
// causes a different series of branches when the m.homeserver
// bit of the JSON is missing.
action = "IGNORE";
}
reject({err: err, response: response, wkAction: action});
return;
}
try {
resolve(JSON.parse(body));
} catch (e) {
console.error(e);
if (e.name === "SyntaxError") {
reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"});
} else throw e;
}
},
);
});
},
_initLoginLogic: function(hsUrl, isUrl) {
const self = this;
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
@ -394,6 +535,7 @@ module.exports = React.createClass({
initialPhoneCountry={this.state.phoneCountry}
initialPhoneNumber={this.state.phoneNumber}
onUsernameChanged={this.onUsernameChanged}
onUsernameBlur={this.onUsernameBlur}
onPhoneCountryChanged={this.onPhoneCountryChanged}
onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick}
@ -403,10 +545,9 @@ module.exports = React.createClass({
);
},
_renderCasStep: function() {
const CasLogin = sdk.getComponent('login.CasLogin');
_renderSsoStep: function(url) {
return (
<CasLogin onSubmit={this.onCasLogin} />
<a href={url} className="mx_Login_sso_link">{ _t('Sign in with single sign-on') }</a>
);
},
@ -418,6 +559,8 @@ module.exports = React.createClass({
const ServerConfig = sdk.getComponent("login.ServerConfig");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
const errorText = this.state.discoveryError || this.state.errorText;
let loginAsGuestJsx;
if (this.props.enableGuest) {
loginAsGuestJsx =
@ -432,8 +575,8 @@ module.exports = React.createClass({
if (!SdkConfig.get()['disable_custom_urls']) {
serverConfig = <ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
customIsUrl={this.state.discoveredIsUrl ||this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
@ -445,16 +588,16 @@ module.exports = React.createClass({
if (theme !== "status") {
header = <h2>{ _t('Sign in') } { loader }</h2>;
} else {
if (!this.state.errorText) {
if (!errorText) {
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
}
}
let errorTextSection;
if (this.state.errorText) {
if (errorText) {
errorTextSection = (
<div className="mx_Login_error">
{ this.state.errorText }
{ errorText }
</div>
);
}

View file

@ -79,7 +79,7 @@ module.exports = React.createClass({
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
let userId = member ? member.userId : fallbackUserId;
const userId = member ? member.userId : fallbackUserId;
if (viewUserOnClick) {
onClick = () => {

View file

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

View file

@ -31,13 +31,13 @@ export default class ChangelogDialog extends React.Component {
componentDidMount() {
const version = this.props.newVersion.split('-');
const version2 = this.props.version.split('-');
if(version == null || version2 == null) return;
if (version == null || version2 == null) return;
// parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]
for(let i=0; i<REPOS.length; i++) {
for (let i=0; i<REPOS.length; i++) {
const oldVersion = version2[2*i];
const newVersion = version[2*i];
request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => {
if(body == null) return;
if (body == null) return;
this.setState({[REPOS[i]]: JSON.parse(body).commits});
});
}
@ -66,7 +66,7 @@ export default class ChangelogDialog extends React.Component {
{this.state[repo].map(this._elementsForCommit)}
</ul>
</div>
)
);
});
const content = (
@ -83,7 +83,7 @@ export default class ChangelogDialog extends React.Component {
button={_t("Update")}
onFinished={this.props.onFinished}
/>
)
);
}
}

View file

@ -26,7 +26,6 @@ import Unread from '../../../Unread';
import classNames from 'classnames';
export default class ChatCreateOrReuseDialog extends React.Component {
constructor(props) {
super(props);
this.onFinished = this.onFinished.bind(this);

View file

@ -57,7 +57,7 @@ export default React.createClass({
let error = null;
if (!this.state.groupId) {
error = _t("Community IDs cannot be empty.");
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
} else if (!/^[a-z0-9=_\-./]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
}
this.setState({

View file

@ -0,0 +1,71 @@
/*
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 dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
export default (props) => {
const _onLogoutClicked = () => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, {
title: _t("Sign out"),
description: _t(
"To avoid losing your chat history, you must export your room keys " +
"before logging out. You will need to go back to the newer version of " +
"Riot to do this",
),
button: _t("Sign out"),
focus: false,
onFinished: (doLogout) => {
if (doLogout) {
dis.dispatch({action: 'logout'});
props.onFinished();
}
},
});
};
const description =
_t("You've previously used a newer version of Riot on %(host)s. " +
"To use this version again with end to end encryption, you will " +
"need to sign out and back in again. ",
{host: props.host},
);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (<BaseDialog className="mx_CryptoStoreTooNewDialog"
contentId='mx_Dialog_content'
title={_t("Incompatible Database")}
hasCancel={false}
onFinished={props.onFinished}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ description }
</div>
<DialogButtons primaryButton={_t('Continue With Encryption Disabled')}
hasCancel={false}
onPrimaryButtonClick={props.onFinished}
>
<button onClick={_onLogoutClicked} >
{ _t('Sign out') }
</button>
</DialogButtons>
</BaseDialog>);
};

View file

@ -101,6 +101,9 @@ export default React.createClass({
},
onSubmit: function(ev) {
if (this.refs.uiAuth) {
this.refs.uiAuth.tryContinue();
}
this.setState({
doingUIAuth: true,
});
@ -217,6 +220,8 @@ export default React.createClass({
onAuthFinished={this._onUIAuthFinished}
inputs={{}}
poll={true}
ref="uiAuth"
continueIsManaged={true}
/>;
}
const inputClasses = classnames({

View file

@ -0,0 +1,309 @@
/*
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 Modal from '../../../../Modal';
import { _t } from '../../../../languageHandler';
/**
* Dialog for restoring e2e keys from a backup and the user's recovery key
*/
export default React.createClass({
getInitialState: function() {
return {
backupInfo: null,
loading: false,
loadError: null,
restoreError: null,
recoveryKey: "",
recoverInfo: null,
recoveryKeyValid: false,
forceRecoveryKey: false,
passPhrase: '',
};
},
componentWillMount: function() {
this._loadBackupStatus();
},
_onCancel: function() {
this.props.onFinished(false);
},
_onDone: function() {
this.props.onFinished(true);
},
_onUseRecoveryKeyClick: function() {
this.setState({
forceRecoveryKey: true,
});
},
_onResetRecoveryClick: function() {
this.props.onFinished(false);
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
{
onFinished: () => {
this._loadBackupStatus();
},
},
);
},
_onRecoveryKeyChange: function(e) {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
});
},
_onPassPhraseNext: async function() {
this.setState({
loading: true,
restoreError: null,
});
try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
this.state.passPhrase, undefined, undefined, this.state.backupInfo.version,
);
this.setState({
loading: false,
recoverInfo,
});
} catch (e) {
console.log("Error restoring backup", e);
this.setState({
loading: false,
restoreError: e,
});
}
},
_onRecoveryKeyNext: async function() {
this.setState({
loading: true,
restoreError: null,
});
try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
this.state.recoveryKey, undefined, undefined, this.state.backupInfo.version,
);
this.setState({
loading: false,
recoverInfo,
});
} catch (e) {
console.log("Error restoring backup", e);
this.setState({
loading: false,
restoreError: e,
});
}
},
_onPassPhraseChange: function(e) {
this.setState({
passPhrase: e.target.value,
});
},
_onPassPhraseKeyPress: function(e) {
if (e.key === "Enter") {
this._onPassPhraseNext();
}
},
_onRecoveryKeyKeyPress: function(e) {
if (e.key === "Enter" && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
},
_loadBackupStatus: async function() {
this.setState({
loading: true,
loadError: null,
});
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.setState({
loadError: null,
loading: false,
backupInfo,
});
} catch (e) {
console.log("Error loading backup status", e);
this.setState({
loadError: e,
loading: false,
});
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent("elements.Spinner");
const backupHasPassphrase = (
this.state.backupInfo &&
this.state.backupInfo.auth_data &&
this.state.backupInfo.auth_data.private_key_salt &&
this.state.backupInfo.auth_data.private_key_iterations
);
let content;
let title;
if (this.state.loading) {
title = _t("Loading...");
content = <Spinner />;
} else if (this.state.loadError) {
title = _t("Error");
content = _t("Unable to load backup status");
} else if (this.state.restoreError) {
title = _t("Error");
content = _t("Unable to restore backup");
} else if (this.state.backupInfo === null) {
title = _t("Error");
content = _t("No backup found!");
} else if (this.state.recoverInfo) {
title = _t("Backup Restored");
let failedToDecrypt;
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
failedToDecrypt = <p>{_t(
"Failed to decrypt %(failedCount)s sessions!",
{failedCount: this.state.recoverInfo.total - this.state.recoverInfo.imported},
)}</p>;
}
content = <div>
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
{failedToDecrypt}
</div>;
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter Recovery Passphrase");
content = <div>
{_t(
"Access your secure message history and set up secure " +
"messaging by entering your recovery passphrase.",
)}<br />
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
<input type="password"
className="mx_RestoreKeyBackupDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
autoFocus={true}
/>
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
/>
</div>
{_t(
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>"
, {}, {
button1: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
button2: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
} else {
title = _t("Enter Recovery Key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let keyStatus;
if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
} else {
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
</div>;
}
content = <div>
{_t(
"Access your secure message history and set up secure " +
"messaging by entering your recovery key.",
)}<br />
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
onKeyPress={this._onRecoveryKeyKeyPress}
value={this.state.recoveryKey}
autoFocus={true}
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
</div>
{_t(
"If you've forgotten your recovery passphrase you can "+
"<button>set up new recovery options</button>"
, {}, {
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
}
return (
<BaseDialog className='mx_RestoreKeyBackupDialog'
onFinished={this.props.onFinished}
title={title}
>
<div>
{content}
</div>
</BaseDialog>
);
},
});

View file

@ -153,8 +153,8 @@ export default class NetworkDropdown extends React.Component {
const sortedInstances = this.props.protocols[proto].instances;
sortedInstances.sort(function(x, y) {
const a = x.desc
const b = y.desc
const a = x.desc;
const b = y.desc;
if (a < b) {
return -1;
} else if (a > b) {
@ -208,7 +208,7 @@ export default class NetworkDropdown extends React.Component {
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
{icon}
<span className="mx_NetworkDropdown_menu_network">{name}</span>
</div>
</div>;
}
render() {
@ -223,11 +223,11 @@ export default class NetworkDropdown extends React.Component {
current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
/>
/>;
} else {
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
current_value = this._makeMenuOption(
this.state.selectedServer, instance, this.state.includeAll, false
this.state.selectedServer, instance, this.state.includeAll, false,
);
}

View file

@ -318,6 +318,19 @@ export default class AppTile extends React.Component {
}
this.setState({deleting: true});
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this.refs.appFrame) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this.refs.appFrame.src = 'about:blank';
}
WidgetUtils.setRoomWidget(
this.props.room.roomId,
this.props.id,

View file

@ -78,7 +78,7 @@ export default React.createClass({
},
render: function() {
let blacklistButton = null, verifyButton = null;
let blacklistButton = null; let verifyButton = null;
if (this.state.device.isBlocked()) {
blacklistButton = (

View file

@ -43,7 +43,11 @@ module.exports = React.createClass({
focus: PropTypes.bool,
// disables the primary and cancel buttons
disabled: PropTypes.bool,
// disables only the primary button
primaryDisabled: PropTypes.bool,
},
getDefaultProps: function() {
@ -70,15 +74,15 @@ module.exports = React.createClass({
}
return (
<div className="mx_Dialog_buttons">
{ cancelButton }
{ this.props.children }
<button className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
disabled={this.props.disabled}
disabled={this.props.disabled || this.props.primaryDisabled}
>
{ this.props.primaryButton }
</button>
{ this.props.children }
{ cancelButton }
</div>
);
},

View file

@ -122,7 +122,6 @@ export default class EditableTextContainer extends React.Component {
);
}
}
}
EditableTextContainer.propTypes = {

View file

@ -16,13 +16,13 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
const MatrixClientPeg = require('../../../MatrixClientPeg');
import {formatDate} from '../../../DateUtils';
var filesize = require('filesize');
var AccessibleButton = require('../../../components/views/elements/AccessibleButton');
const filesize = require('filesize');
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
const Modal = require('../../../Modal');
const sdk = require('../../../index');
import { _t } from '../../../languageHandler';
@ -69,24 +69,24 @@ module.exports = React.createClass({
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
onFinished: (proceed) => {
if (!proceed) return;
var self = this;
const self = this;
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
).catch(function(e) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this.
var code = e.errcode || e.statusCode;
const code = e.errcode || e.statusCode;
Modal.createTrackedDialog('You cannot delete this image.', '', ErrorDialog, {
title: _t('Error'),
description: _t('You cannot delete this image. (%(code)s)', {code: code})
description: _t('You cannot delete this image. (%(code)s)', {code: code}),
});
}).done();
}
},
});
},
getName: function () {
var name = this.props.name;
getName: function() {
let name = this.props.name;
if (name && this.props.link) {
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
}
@ -94,7 +94,6 @@ module.exports = React.createClass({
},
render: function() {
/*
// In theory max-width: 80%, max-height: 80% on the CSS should work
// but in practice, it doesn't, so do it manually:
@ -123,7 +122,7 @@ module.exports = React.createClass({
height: displayHeight
};
*/
var style, res;
let style; let res;
if (this.props.width && this.props.height) {
style = {
@ -133,23 +132,22 @@ module.exports = React.createClass({
res = style.width + "x" + style.height + "px";
}
var size;
let size;
if (this.props.fileSize) {
size = filesize(this.props.fileSize);
}
var size_res;
let size_res;
if (size && res) {
size_res = size + ", " + res;
}
else {
} else {
size_res = size || res;
}
var showEventMeta = !!this.props.mxEvent;
const showEventMeta = !!this.props.mxEvent;
var eventMeta;
if(showEventMeta) {
let eventMeta;
if (showEventMeta) {
// Figure out the sender, defaulting to mxid
let sender = this.props.mxEvent.getSender();
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
@ -163,8 +161,8 @@ module.exports = React.createClass({
</div>);
}
var eventRedact;
if(showEventMeta) {
let eventRedact;
if (showEventMeta) {
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
{ _t('Remove') }
</div>);
@ -175,10 +173,10 @@ module.exports = React.createClass({
<div className="mx_ImageView_lhs">
</div>
<div className="mx_ImageView_content">
<img src={this.props.src} style={style}/>
<img src={this.props.src} style={style} />
<div className="mx_ImageView_labelWrapper">
<div className="mx_ImageView_label">
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }><img src="img/cancel-white.svg" width="18" height="18" alt={ _t('Close') }/></AccessibleButton>
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }><img src="img/cancel-white.svg" width="18" height="18" alt={ _t('Close') } /></AccessibleButton>
<div className="mx_ImageView_shim">
</div>
<div className="mx_ImageView_name">
@ -187,7 +185,7 @@ module.exports = React.createClass({
{ eventMeta }
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
<div className="mx_ImageView_download">
{ _t('Download this file') }<br/>
{ _t('Download this file') }<br />
<span className="mx_ImageView_size">{ size_res }</span>
</div>
</a>
@ -201,5 +199,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -20,14 +20,14 @@ module.exports = React.createClass({
displayName: 'InlineSpinner',
render: function() {
var w = this.props.w || 16;
var h = this.props.h || 16;
var imgClass = this.props.imgClassName || "";
const w = this.props.w || 16;
const h = this.props.h || 16;
const imgClass = this.props.imgClassName || "";
return (
<div className="mx_InlineSpinner">
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
</div>
);
}
},
});

View file

@ -54,7 +54,6 @@ function getOrCreateContainer(containerId) {
* bounding rect as the parent of PE.
*/
export default class PersistedElement extends React.Component {
static propTypes = {
// Unique identifier for this PersistedElement instance
// Any PersistedElements with the same persistKey will use

View file

@ -29,7 +29,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^\/]*)$/;
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
const Pill = React.createClass({
statics: {

View file

@ -16,19 +16,19 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
module.exports = React.createClass({
displayName: 'Spinner',
render: function() {
var w = this.props.w || 32;
var h = this.props.h || 32;
var imgClass = this.props.imgClassName || "";
const w = this.props.w || 32;
const h = this.props.h || 32;
const imgClass = this.props.imgClassName || "";
return (
<div className="mx_Spinner">
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
</div>
);
}
},
});

View file

@ -20,7 +20,6 @@ import TintableSvg from './TintableSvg';
import AccessibleButton from './AccessibleButton';
export default class TintableSvgButton extends React.Component {
constructor(props) {
super(props);
}

View file

@ -39,7 +39,7 @@ module.exports = React.createClass({
<div className="mx_MatrixToolbar_content">
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
</div>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/></AccessibleButton>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" alt={_t('Close')} /></AccessibleButton>
</div>
);
},

View file

@ -45,10 +45,10 @@ export default React.createClass({
description: <div className="mx_MatrixToolbar_changelog">{releaseNotes}</div>,
button: _t("Update"),
onFinished: (update) => {
if(update && PlatformPeg.get()) {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
}
},
});
},
@ -58,10 +58,10 @@ export default React.createClass({
version: this.props.version,
newVersion: this.props.newVersion,
onFinished: (update) => {
if(update && PlatformPeg.get()) {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
}
},
});
},
@ -103,5 +103,5 @@ export default React.createClass({
{action_button}
</div>
);
}
},
});

View file

@ -32,14 +32,14 @@ export default React.createClass({
getDefaultProps: function() {
return {
detail: '',
}
};
},
getStatusText: function() {
// we can't import the enum from riot-web as we don't want matrix-react-sdk
// to depend on riot-web. so we grab it as a normal object via API instead.
const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum();
switch(this.props.status) {
switch (this.props.status) {
case updateCheckStatusEnum.ERROR:
return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail });
case updateCheckStatusEnum.CHECKING:
@ -59,7 +59,7 @@ export default React.createClass({
const message = this.getStatusText();
const warning = _t('Warning');
if (!'getUpdateCheckStatusEnum' in PlatformPeg.get()) {
if (!('getUpdateCheckStatusEnum' in PlatformPeg.get())) {
return <div></div>;
}
@ -83,9 +83,9 @@ export default React.createClass({
{message}
</div>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.hideToolbar}>
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/>
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')} />
</AccessibleButton>
</div>
);
}
},
});

View file

@ -165,7 +165,7 @@ export default React.createClass({
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true}>
{ joined }
{ invited }
</GeminiScrollbarWrapper>

View file

@ -22,6 +22,7 @@ import classnames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -209,6 +210,145 @@ export const RecaptchaAuthEntry = React.createClass({
},
});
export const TermsAuthEntry = React.createClass({
displayName: 'TermsAuthEntry',
statics: {
LOGIN_TYPE: "m.login.terms",
},
propTypes: {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
},
componentWillMount: function() {
// example stageParams:
//
// {
// "policies": {
// "privacy_policy": {
// "version": "1.0",
// "en": {
// "name": "Privacy Policy",
// "url": "https://example.org/privacy-1.0-en.html",
// },
// "fr": {
// "name": "Politique de confidentialité",
// "url": "https://example.org/privacy-1.0-fr.html",
// },
// },
// "other_policy": { ... },
// }
// }
const allPolicies = this.props.stageParams.policies || {};
const prefLang = SettingsStore.getValue("language");
const initToggles = {};
const pickedPolicies = [];
for (const policyId of Object.keys(allPolicies)) {
const policy = allPolicies[policyId];
// Pick a language based on the user's language, falling back to english,
// and finally to the first language available. If there's still no policy
// available then the homeserver isn't respecting the spec.
let langPolicy = policy[prefLang];
if (!langPolicy) langPolicy = policy["en"];
if (!langPolicy) {
// last resort
const firstLang = Object.keys(policy).find(e => e !== "version");
langPolicy = policy[firstLang];
}
if (!langPolicy) throw new Error("Failed to find a policy to show the user");
initToggles[policyId] = false;
langPolicy.id = policyId;
pickedPolicies.push(langPolicy);
}
this.setState({
"toggledPolicies": initToggles,
"policies": pickedPolicies,
});
},
tryContinue: function() {
this._trySubmit();
},
_togglePolicy: function(policyId) {
const newToggles = {};
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
if (policy.id === policyId) checked = !checked;
newToggles[policy.id] = checked;
}
this.setState({"toggledPolicies": newToggles});
},
_trySubmit: function() {
let allChecked = true;
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
allChecked = allChecked && checked;
}
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
},
render: function() {
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
}
const checkboxes = [];
let allChecked = true;
for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id];
allChecked = allChecked && checked;
checkboxes.push(
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onClick={() => this._togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
</label>,
);
}
let errorSection;
if (this.props.errorText || this.state.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText || this.state.errorText }
</div>
);
}
let submitButton;
if (this.props.showContinue !== false) {
// XXX: button classes
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_UserSettings_button"
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
}
return (
<div>
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
{ checkboxes }
{ errorSection }
{ submitButton }
</div>
);
},
});
export const EmailIdentityAuthEntry = React.createClass({
displayName: 'EmailIdentityAuthEntry',
@ -496,6 +636,7 @@ const AuthEntryComponents = [
RecaptchaAuthEntry,
EmailIdentityAuthEntry,
MsisdnAuthEntry,
TermsAuthEntry,
];
export function getEntryComponentForLoginType(loginType) {

View file

@ -30,6 +30,7 @@ class PasswordLogin extends React.Component {
static defaultProps = {
onError: function() {},
onUsernameChanged: function() {},
onUsernameBlur: function() {},
onPasswordChanged: function() {},
onPhoneCountryChanged: function() {},
onPhoneNumberChanged: function() {},
@ -53,6 +54,7 @@ class PasswordLogin extends React.Component {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.onUsernameChanged = this.onUsernameChanged.bind(this);
this.onUsernameBlur = this.onUsernameBlur.bind(this);
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
@ -124,6 +126,10 @@ class PasswordLogin extends React.Component {
this.props.onUsernameChanged(ev.target.value);
}
onUsernameBlur(ev) {
this.props.onUsernameBlur(this.state.username);
}
onLoginTypeChange(loginType) {
this.props.onError(null); // send a null error to clear any error messages
this.setState({
@ -167,6 +173,7 @@ class PasswordLogin extends React.Component {
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
placeholder="joe@example.com"
value={this.state.username}
autoFocus
@ -182,6 +189,7 @@ class PasswordLogin extends React.Component {
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
placeholder={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),

View file

@ -70,6 +70,23 @@ module.exports = React.createClass({
};
},
componentWillReceiveProps: function(newProps) {
if (newProps.customHsUrl === this.state.hs_url &&
newProps.customIsUrl === this.state.is_url) return;
this.setState({
hs_url: newProps.customHsUrl,
is_url: newProps.customIsUrl,
configVisible: !newProps.withToggleButton ||
(newProps.customHsUrl !== newProps.defaultHsUrl) ||
(newProps.customIsUrl !== newProps.defaultIsUrl),
});
this.props.onServerConfigChange({
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
});
},
onHomeserverChanged: function(ev) {
this.setState({hs_url: ev.target.value}, function() {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {

View file

@ -278,6 +278,7 @@ export default class MImageBody extends React.Component {
let img = null;
let placeholder = null;
let gifLabel = null;
// e2e image hasn't been decrypted yet
if (content.file !== undefined && this.state.decryptedUrl === null) {
@ -302,11 +303,14 @@ export default class MImageBody extends React.Component {
onMouseLeave={this.onImageLeave} />;
}
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
}
const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px" }} >
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
<div style={{ paddingBottom: (100 * infoHeight / infoWidth) + '%' }} />
{ showPlaceholder &&
<div className="mx_MImageBody_thumbnail" style={{
// Constrain width here so that spinner appears central to the loaded thumbnail
@ -320,6 +324,7 @@ export default class MImageBody extends React.Component {
<div style={{display: !showPlaceholder ? undefined : 'none'}}>
{ img }
{ gifLabel }
</div>
{ this.state.hover && this.getTooltip() }

View file

@ -103,7 +103,7 @@ module.exports = React.createClass({
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
}
let newCanonicalAlias = this.state.canonicalAlias;
const newCanonicalAlias = this.state.canonicalAlias;
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
console.log("AliasSettings: Updating canonical alias");
@ -167,7 +167,7 @@ module.exports = React.createClass({
if (!this.props.canonicalAlias) {
this.setState({
canonicalAlias: alias
canonicalAlias: alias,
});
}
},
@ -220,8 +220,9 @@ module.exports = React.createClass({
let canonical_alias_section;
if (this.props.canSetCanonicalAlias) {
let found = false;
const canonicalValue = this.state.canonicalAlias || "";
canonical_alias_section = (
<select onChange={this.onCanonicalAliasChange} value={this.state.canonicalAlias}>
<select onChange={this.onCanonicalAliasChange} value={canonicalValue}>
<option value="" key="unset">{ _t('not specified') }</option>
{
Object.keys(self.state.domainToAliases).map((domain, i) => {

View file

@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import isEqual from 'lodash/isEqual';
const GROUP_ID_REGEX = /\+\S+\:\S+/;
const GROUP_ID_REGEX = /\+\S+:\S+/;
module.exports = React.createClass({
displayName: 'RelatedGroupSettings',

Some files were not shown because too many files have changed in this diff Show more