mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 21:24:59 +08:00
Merge remote-tracking branch 'upstream/develop' into hs/upload-limits
This commit is contained in:
commit
2b077b4f5d
2
.babelrc
2
.babelrc
@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["react", "es2015", "es2016"],
|
||||
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports"]
|
||||
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports", "syntax-dynamic-import"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
132
CHANGELOG.md
132
CHANGELOG.md
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
11
package.json
11
package.json
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
25
res/css/views/dialogs/_CreateKeyBackupDialog.scss
Normal file
25
res/css/views/dialogs/_CreateKeyBackupDialog.scss
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
39
res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss
Normal file
39
res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss
Normal 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;
|
||||
}
|
29
res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss
Normal file
29
res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss
Normal 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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.mx_MemberDeviceInfo.mx_DeviceVerifyButtons {
|
||||
padding: 6px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
@ -111,6 +111,3 @@ limitations under the License.
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_MemberList_outerWrapper {
|
||||
height: 0px;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -56,3 +56,7 @@ limitations under the License.
|
||||
.mx_RoomPreviewBar_warningIcon {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mx_RoomPreviewBar_spinnerIntro {
|
||||
margin-top: 50px;
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -78,7 +78,6 @@ class MemberEntity extends Entity {
|
||||
}
|
||||
|
||||
class UserEntity extends Entity {
|
||||
|
||||
constructor(model, showInviteButton, inviteFn) {
|
||||
super(model);
|
||||
this.showInviteButton = Boolean(showInviteButton);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
48
src/Modal.js
48
src/Modal.js
@ -23,6 +23,7 @@ import PropTypes from 'prop-types';
|
||||
import Analytics from './Analytics';
|
||||
import sdk from './index';
|
||||
import dis from './dispatcher';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||
|
||||
@ -32,15 +33,15 @@ const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||
*/
|
||||
const AsyncWrapper = React.createClass({
|
||||
propTypes: {
|
||||
/** A function which takes a 'callback' argument which it will call
|
||||
* with the real component once it loads.
|
||||
/** A promise which resolves with the real component
|
||||
*/
|
||||
loader: PropTypes.func.isRequired,
|
||||
prom: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
component: null,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
@ -49,14 +50,18 @@ const AsyncWrapper = React.createClass({
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Starting load of AsyncWrapper for modal');
|
||||
this.props.loader((e) => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('AsyncWrapper load completed with '+e.displayName);
|
||||
this.props.prom.then((result) => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({component: e});
|
||||
// Take the 'default' member if it's there, then we support
|
||||
// passing in just an import()ed module, since ES6 async import
|
||||
// always returns a module *namespace*.
|
||||
const component = result.default ? result.default : result;
|
||||
this.setState({component});
|
||||
}).catch((e) => {
|
||||
console.warn('AsyncWrapper promise failed', e);
|
||||
this.setState({error: e});
|
||||
});
|
||||
},
|
||||
|
||||
@ -64,11 +69,27 @@ const AsyncWrapper = React.createClass({
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_onWrapperCancelClick: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const {loader, ...otherProps} = this.props;
|
||||
if (this.state.component) {
|
||||
const Component = this.state.component;
|
||||
return <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;
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -22,7 +22,6 @@ const SdkConfig = require('./SdkConfig');
|
||||
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||
|
||||
class ScalarAuthClient {
|
||||
|
||||
constructor() {
|
||||
this.scalarToken = null;
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ const DEFAULTS = {
|
||||
};
|
||||
|
||||
class SdkConfig {
|
||||
|
||||
static get() {
|
||||
return global.mxReactSdkConfig || {};
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
@ -85,7 +85,7 @@ export default class Autocompleter {
|
||||
provider
|
||||
.getCompletions(query, selection, force)
|
||||
.timeout(PROVIDER_COMPLETION_TIMEOUT)
|
||||
.reflect()
|
||||
.reflect(),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
|
@ -26,7 +26,6 @@ import { Block } from 'slate';
|
||||
*/
|
||||
|
||||
class PlainWithPillsSerializer {
|
||||
|
||||
/*
|
||||
* @param {String} options.pillFormat - either 'md', 'plain', 'id'
|
||||
*/
|
||||
|
@ -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} />;
|
||||
}
|
||||
},
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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() {
|
||||
|
@ -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});
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
{ 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] : "");
|
||||
}
|
||||
|
@ -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> " +
|
||||
|
@ -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 = (
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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.
|
||||
},
|
||||
|
||||
|
@ -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]}`;
|
||||
}
|
||||
|
||||
|
@ -53,5 +53,5 @@ module.exports = React.createClass({
|
||||
</SyntaxHighlight>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 = () => {
|
||||
|
@ -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 :(
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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({
|
||||
|
71
src/components/views/dialogs/CryptoStoreTooNewDialog.js
Normal file
71
src/components/views/dialogs/CryptoStoreTooNewDialog.js
Normal 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>);
|
||||
};
|
@ -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({
|
||||
|
309
src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
Normal file
309
src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 = (
|
||||
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
@ -122,7 +122,6 @@ export default class EditableTextContainer extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EditableTextContainer.propTypes = {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -20,7 +20,6 @@ import TintableSvg from './TintableSvg';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
|
||||
export default class TintableSvgButton extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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?:\/\//, ''),
|
||||
|
@ -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() {
|
||||
|
@ -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() }
|
||||
|
@ -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) => {
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user