Merge branch 'experimental' into bwindels/roomgridview-experimental

This commit is contained in:
Bruno Windels 2019-01-07 14:17:57 +01:00
commit 290dc9d8fb
146 changed files with 5292 additions and 1051 deletions

View File

@ -47,6 +47,9 @@ module.exports = {
}],
"react/jsx-key": ["error"],
// Components in JSX should always be defined.
"react/jsx-no-undef": "error",
// Assert no spacing in JSX curly brackets
// <Element prop={ consideredError} prop={notConsideredError} />
//

View File

@ -30,7 +30,7 @@ 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
scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests 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

View File

@ -1,3 +1,57 @@
Changes in [0.14.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7) (2018-12-10)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.2...v0.14.7)
* No changes since rc.2
Changes in [0.14.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.2) (2018-12-06)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.1...v0.14.7-rc.2)
* Ship the babelrc file to npm
[\#2332](https://github.com/matrix-org/matrix-react-sdk/pull/2332)
Changes in [0.14.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.1) (2018-12-06)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.6...v0.14.7-rc.1)
* Suppress CORS errors in the 'failed to join room' dialog
[\#2306](https://github.com/matrix-org/matrix-react-sdk/pull/2306)
* Check if users exist before inviting them and communicate errors
[\#2317](https://github.com/matrix-org/matrix-react-sdk/pull/2317)
* Update from Weblate.
[\#2328](https://github.com/matrix-org/matrix-react-sdk/pull/2328)
* Allow group summary to load when /users fails
[\#2326](https://github.com/matrix-org/matrix-react-sdk/pull/2326)
* Show correct text if passphrase is skipped
[\#2324](https://github.com/matrix-org/matrix-react-sdk/pull/2324)
* Add password strength meter to backup creation UI
[\#2294](https://github.com/matrix-org/matrix-react-sdk/pull/2294)
* Check upload limits before trying to upload large files
[\#1876](https://github.com/matrix-org/matrix-react-sdk/pull/1876)
* Support .well-known discovery
[\#2227](https://github.com/matrix-org/matrix-react-sdk/pull/2227)
* Make create key backup dialog async
[\#2291](https://github.com/matrix-org/matrix-react-sdk/pull/2291)
* Forgot to enable continue button on download
[\#2288](https://github.com/matrix-org/matrix-react-sdk/pull/2288)
* Online incremental megolm backups (v2)
[\#2169](https://github.com/matrix-org/matrix-react-sdk/pull/2169)
* Add recovery key download button
[\#2284](https://github.com/matrix-org/matrix-react-sdk/pull/2284)
* Passphrase Support for e2e backups
[\#2283](https://github.com/matrix-org/matrix-react-sdk/pull/2283)
* Update async dialog interface to use promises
[\#2286](https://github.com/matrix-org/matrix-react-sdk/pull/2286)
* Support for m.login.sso
[\#2279](https://github.com/matrix-org/matrix-react-sdk/pull/2279)
* Added badge to non-autoplay GIFs
[\#2235](https://github.com/matrix-org/matrix-react-sdk/pull/2235)
* Improve terms auth flow
[\#2277](https://github.com/matrix-org/matrix-react-sdk/pull/2277)
* Handle crypto db version upgrade
[\#2282](https://github.com/matrix-org/matrix-react-sdk/pull/2282)
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)

View File

@ -1,4 +1,4 @@
Contributing code to The React SDK
==================================
matrix-react-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst

View File

@ -128,60 +128,35 @@ Github Issues
All issues should be filed under https://github.com/vector-im/riot-web/issues
for now.
OUTDATED: To Create Your Own Skin
=================================
Development
===========
**This is ALL LIES currently, and needs to be updated**
Ensure you have the latest stable Node JS runtime installed (v8.x is the best choice). Then check out
the code and pull in dependencies:
Skins are modules are exported from such a package in the `lib` directory.
`lib/skins` contains one directory per-skin, named after the skin, and the
`modules` directory contains modules as their javascript files.
```bash
git clone https://github.com/matrix-org/matrix-react-sdk.git
cd matrix-react-sdk
git checkout develop
npm install
```
A basic skin is provided in the matrix-react-skin package. This also contains
a minimal application that instantiates the basic skin making a working matrix
client.
`matrix-react-sdk` depends on `matrix-js-sdk`. To make use of changes in the
latter and to ensure tests run against the develop branch of `matrix-js-sdk`,
you should run the following which will sync changes from the JS sdk here.
You can use matrix-react-sdk directly, but to do this you would have to provide
'views' for each UI component. To get started quickly, use matrix-react-skin.
```bash
npm link ../matrix-js-sdk
```
To actually change the look of a skin, you can create a base skin (which
does not use views from any other skin) or you can make a derived skin.
Note that derived skins are currently experimental: for example, the CSS
from the skins it is based on will not be automatically included.
Command assumes a checked out and installed `matrix-js-sdk` folder in parent
folder.
To make a skin, create React classes for any custom components you wish to add
in a skin within `src/skins/<skin name>`. These can be based off the files in
`views` in the `matrix-react-skin` package, modifying the require() statement
appropriately.
Running tests
=============
If you make a derived skin, you only need copy the files you wish to customise.
Ensure you've followed the above development instructions and then:
Once you've made all your view files, you need to make a `skinfo.json`. This
contains all the metadata for a skin. This is a JSON file with, currently, a
single key, 'baseSkin'. Set this to the empty string if your skin is a base skin,
or for a derived skin, set it to the path of your base skin's skinfo.json file, as
you would use in a require call.
Now you have the basis of a skin, you need to generate a skindex.json file. The
`reskindex.js` tool in matrix-react-sdk does this for you. It is suggested that
you add an npm script to run this, as in matrix-react-skin.
For more specific detail on any of these steps, look at matrix-react-skin.
Alternative instructions:
* Create a new NPM project. Be sure to directly depend on react, (otherwise
you can end up with two copies of react).
* Create an index.js file that sets up react. Add require statements for
React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the
SDK and call Render. This can be a skin provided by a separate package or
a skin in the same package.
* Add a way to build your project: we suggest copying the scripts block
from matrix-react-skin (which uses babel and webpack). You could use
different tools but remember that at least the skins and modules of
your project should end up in plain (ie. non ES6, non JSX) javascript in
the lib directory at the end of the build process, as well as any
packaging that you might do.
* Create an index.html file pulling in your compiled javascript and the
CSS bundle from the skin you use. For now, you'll also need to manually
import CSS from any skins that your skin inherts from.
```bash
npm run test
```

View File

@ -165,7 +165,6 @@ ECMAScript
React
-----
- Use React.createClass rather than ES6 classes for components, as the boilerplate is way too heavy on ES6 currently. ES7 might improve it.
- Pull out functions in props to the class, generally as specific event handlers:
```jsx
@ -174,11 +173,38 @@ React
<Foo onClick={this.doStuff}> // Better
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
```
Not doing so is acceptable in a single case; in function-refs:
Not doing so is acceptable in a single case: in function-refs:
```jsx
<Foo ref={(self) => this.component = self}>
```
- Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass`
- You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor):
```js
class Widget extends React.Component
onFooClick = () => {
...
}
}
```
- To define `propTypes`, use a static property:
```js
class Widget extends React.Component
static propTypes = {
...
}
}
```
- If you need to specify initial component state, [assign it](https://reactjs.org/docs/react-component.html#constructor) to `this.state` in the constructor:
```js
constructor(props) {
super(props);
// Don't call this.setState() here!
this.state = { counter: 0 };
}
```
- Think about whether your component really needs state: are you duplicating
information in component state that could be derived from the model?

View File

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.14.6",
"version": "0.14.7",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -10,6 +10,7 @@
"license": "Apache-2.0",
"main": "lib/index.js",
"files": [
".babelrc",
".eslintrc.js",
"CHANGELOG.md",
"CONTRIBUTING.rst",
@ -72,11 +73,12 @@
"gfm.css": "^1.1.1",
"glob": "^5.0.14",
"highlight.js": "^9.13.0",
"is-ip": "^2.0.0",
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.6",
"lodash": "^4.13.1",
"lolex": "2.3.2",
"matrix-js-sdk": "0.14.1",
"matrix-js-sdk": "0.14.2",
"optimist": "^0.6.1",
"pako": "^1.0.5",
"prop-types": "^15.5.8",
@ -96,7 +98,8 @@
"text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0",
"velocity-vector": "github:vector-im/velocity#059e3b2",
"whatwg-fetch": "^1.1.1"
"whatwg-fetch": "^1.1.1",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"babel-cli": "^6.26.0",
@ -121,8 +124,9 @@
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^7.7.0",
"estree-walker": "^0.5.0",
"expect": "^1.16.0",
"expect": "^23.6.0",
"flow-parser": "^0.57.3",
"jest-mock": "^23.2.0",
"karma": "^3.0.0",
"karma-chrome-launcher": "^0.2.3",
"karma-cli": "^1.0.1",

View File

@ -32,7 +32,7 @@ body {
margin: 0px;
}
div.error, div.warning {
.error, .warning {
color: $warning-color;
}
@ -47,7 +47,7 @@ h2 {
a:hover,
a:link,
a:visited {
color: $accent-color;
color: $accent-color-alt;
}
input[type=text], input[type=password], textarea {
@ -301,7 +301,7 @@ textarea {
}
.mx_textButton {
@mixin mx_DialogButton_small;
@mixin mx_DialogButton_small;
}
.mx_textButton:hover {

View File

@ -26,8 +26,10 @@
@import "./structures/_ViewSource.scss";
@import "./structures/login/_Login.scss";
@import "./views/avatars/_BaseAvatar.scss";
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_RoomTileContextMenu.scss";
@import "./views/context_menus/_StatusMessageContextMenu.scss";
@import "./views/context_menus/_TagTileContextMenu.scss";
@import "./views/context_menus/_TopLeftMenu.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@ -36,7 +38,6 @@
@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";
@ -50,6 +51,7 @@
@import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss";
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
@import "./views/directory/_NetworkDropdown.scss";
@import "./views/elements/_AccessibleButton.scss";
@ -105,6 +107,7 @@
@import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomSettings.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTooltip.scss";

View File

@ -89,12 +89,6 @@ limitations under the License.
pointer-events: none;
}
.mx_LeftPanel_container.collapsed .mx_RoleButton {
margin-right: 0px ! important;
padding-top: 3px ! important;
padding-bottom: 3px ! important;
}
.mx_BottomLeftMenu_options > div {
display: inline-block;
}

View File

@ -45,7 +45,8 @@ limitations under the License.
cursor: pointer;
flex: 0 0 auto;
vertical-align: top;
padding-left: 4px;
margin-top: 4px;
padding-left: 5px;
padding-right: 5px;
text-align: center;
position: relative;
@ -57,7 +58,7 @@ limitations under the License.
}
.mx_RightPanel_headerButton_highlight {
border-color: $accent-color;
border-color: $button-bg-color;
}
.mx_RightPanel_headerButton_badge {

View File

@ -19,14 +19,14 @@ limitations under the License.
each with a flex-shrink difference of 4 order of magnitude,
so they ideally wouldn't affect each other.
lowest category: .mx_RoomSubList
flex:-shrink: 10000000
flex-shrink: 10000000
distribute size of items within the same categery by their size
middle category: .mx_RoomSubList.resized-sized
flex:-shrink: 1000
flex-shrink: 1000
applied when using the resizer, will have a max-height set to it,
to limit the size
highest category: .mx_RoomSubList.resized-all
flex:-shrink: 1
flex-shrink: 1
small flex-shrink value (1), is only added if you can drag the resizer so far
so in practice you can only assign this category if there is enough space.
*/
@ -39,7 +39,7 @@ limitations under the License.
}
.mx_RoomSubList_nonEmpty {
min-height: 76px;
min-height: 70px;
.mx_AutoHideScrollbar_offset {
padding-bottom: 4px;
@ -94,7 +94,7 @@ limitations under the License.
font-weight: 600;
font-size: 12px;
padding: 0 5px;
background-color: $accent-color;
background-color: $roomtile-name-color;
}
.mx_RoomSubList_addRoom, .mx_RoomSubList_badge {
@ -154,7 +154,7 @@ limitations under the License.
position: sticky;
left: 0;
right: 0;
height: 40px;
height: 30px;
content: "";
display: block;
z-index: 100;
@ -162,20 +162,20 @@ limitations under the License.
}
&.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset {
margin-top: -40px;
margin-top: -30px;
}
&.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset {
margin-bottom: -40px;
margin-bottom: -30px;
}
&.mx_IndicatorScrollbar_topOverflow::before {
top: 0;
background: linear-gradient($secondary-accent-color, transparent);
background: linear-gradient(to top, rgba(242,245,248,0), rgba(242,245,248,1));
}
&.mx_IndicatorScrollbar_bottomOverflow::after {
bottom: 0;
background: linear-gradient(transparent, $secondary-accent-color);
background: linear-gradient(to bottom, rgba(242,245,248,0), rgba(242,245,248,1));
}
}

View File

@ -124,10 +124,23 @@ limitations under the License.
padding-right: 4px;
}
.mx_TagPanel_groupsButton {
flex: 0;
margin: 17px 0 3px 0;
}
.mx_TagPanel_groupsButton > .mx_GroupsButton:before {
mask: url('../../img/feather-icons/users.svg');
mask-position: center 11px;
}
.mx_TagPanel_groupsButton > .mx_TagPanel_report:before {
mask: url('../../img/feather-icons/life-buoy.svg');
mask-position: center 9px;
}
.mx_TagPanel_groupsButton > .mx_AccessibleButton {
flex: auto;
margin-bottom: 17px;
margin-top: 18px;
margin-bottom: 12px;
height: 40px;
width: 40px;
border-radius: 20px;
@ -138,9 +151,7 @@ limitations under the License.
&:before {
background-color: $tagpanel-bg-color;
mask: url('../../img/icons-groups-nobg.svg');
mask-repeat: no-repeat;
mask-position: center 8px;
content: '';
position: absolute;
top: 0;

View File

@ -14,12 +14,8 @@ 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;
.mx_MemberStatusMessageAvatar_hasStatus {
border: 2px solid $accent-color;
border-radius: 40px;
padding-right: 0 !important; /* Override AccessibleButton styling */
}

View File

@ -0,0 +1,55 @@
/*
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_StatusMessageContextMenu_message {
display: inline-block;
border-radius: 3px 0 0 3px;
border: 1px solid $input-border-color;
font-size: 13px;
padding: 7px 7px 7px 9px;
width: 135px;
background-color: $primary-bg-color !important;
}
.mx_StatusMessageContextMenu_submit {
display: inline-block;
}
.mx_StatusMessageContextMenu_submitFaded {
opacity: 0.5;
}
.mx_StatusMessageContextMenu_submit img {
vertical-align: middle;
margin-left: 8px;
}
.mx_StatusMessageContextMenu hr {
border: 0.5px solid $menu-border-color;
}
.mx_StatusMessageContextMenu_clearIcon {
margin: 5px 15px 5px 5px;
vertical-align: middle;
}
.mx_StatusMessageContextMenu_clear {
padding: 2px;
}
.mx_StatusMessageContextMenu_hasStatus .mx_StatusMessageContextMenu_clear {
color: $warning-color;
}

View File

@ -13,27 +13,79 @@ 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 .mx_Dialog_title {
/* TODO: Consider setting this for all dialog titles. */
margin-bottom: 1em;
}
.mx_CreateKeyBackupDialog_primaryContainer {
/*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/
padding: 20px
}
.mx_CreateKeyBackupDialog_primaryContainer::after {
content: "";
clear: both;
display: block;
}
.mx_CreateKeyBackupDialog_passPhraseContainer {
display: flex;
align-items: start;
}
.mx_CreateKeyBackupDialog_passPhraseHelp {
flex: 1;
height: 85px;
margin-left: 20px;
font-size: 80%;
}
.mx_CreateKeyBackupDialog_passPhraseHelp progress {
width: 100%;
}
.mx_CreateKeyBackupDialog_passPhraseInput {
width: 300px;
flex: none;
width: 250px;
border: 1px solid $accent-color;
border-radius: 5px;
padding: 10px;
margin-bottom: 1em;
}
.mx_CreateKeyBackupDialog_passPhraseMatch {
float: right;
margin-left: 20px;
}
.mx_CreateKeyBackupDialog_recoveryKeyButtons {
float: right;
.mx_CreateKeyBackupDialog_recoveryKeyHeader {
margin-bottom: 1em;
}
.mx_CreateKeyBackupDialog_recoveryKeyContainer {
display: flex;
}
.mx_CreateKeyBackupDialog_recoveryKey {
width: 300px;
width: 262px;
padding: 20px;
color: $info-plinth-fg-color;
background-color: $info-plinth-bg-color;
margin-right: 12px;
}
.mx_CreateKeyBackupDialog_recoveryKeyButtons {
flex: 1;
display: flex;
align-items: center;
}
.mx_CreateKeyBackupDialog_recoveryKeyButtons button {
flex: 1;
white-space: nowrap;
}

View File

@ -0,0 +1,41 @@
/*
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_NewRecoveryMethodDialog .mx_Dialog_title {
margin-bottom: 32px;
}
.mx_NewRecoveryMethodDialog_title {
position: relative;
padding-left: 45px;
padding-bottom: 10px;
&:before {
mask: url("../../../img/e2e/lock-warning.svg");
mask-repeat: no-repeat;
background-color: $primary-fg-color;
content: "";
position: absolute;
top: -6px;
right: 0;
bottom: 0;
left: 0;
}
}
.mx_NewRecoveryMethodDialog .mx_Dialog_buttons {
margin-top: 36px;
}

View File

@ -17,21 +17,30 @@ limitations under the License.
.mx_ResizeHandle {
cursor: row-resize;
flex: 0 0 auto;
background: $panel-divider-color;
background-clip: content-box;
z-index: 100;
}
.mx_ResizeHandle.mx_ResizeHandle_horizontal {
width: 1px;
margin: 0 -5px;
padding: 0 5px;
cursor: col-resize;
}
.mx_ResizeHandle.mx_ResizeHandle_vertical {
height: 1px;
margin: -5px 0;
padding: 5px 0;
cursor: row-resize;
}
.mx_ResizeHandle > div {
background: $panel-divider-color;
}
.mx_ResizeHandle.mx_ResizeHandle_horizontal > div {
width: 1px;
height: 100%;
}
.mx_ResizeHandle.mx_ResizeHandle_vertical > div {
height: 1px;
}

View File

@ -107,3 +107,10 @@ limitations under the License.
}
*/
.mx_EntityTile_subtext {
font-size: 11px;
opacity: 0.5;
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
}

View File

@ -445,7 +445,8 @@ limitations under the License.
}
.mx_EventTile_content .markdown-body a {
color: $accent-color;
color: $accent-color-alt;
text-decoration: underline;
}
.mx_EventTile_content .markdown-body .hljs {

View File

@ -132,6 +132,13 @@ limitations under the License.
margin-left: 8px;
}
.mx_MemberInfo_statusMessage {
font-size: 11px;
opacity: 0.5;
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
}
.mx_MemberInfo .mx_MemberInfo_scrollContainer {
flex: 1;
}

View File

@ -87,7 +87,7 @@ limitations under the License.
.mx_MemberList_invite span {
margin: 0 auto;
background-image: url('../../img/icon-invite-people.svg');
background-image: url('../../img/feather-icons/user-add.svg');
background-repeat: no-repeat;
background-position: center left;
padding-left: 25px;

View File

@ -59,8 +59,8 @@ limitations under the License.
.mx_RoomHeader_buttons {
display: flex;
align-items: center;
margin-top: 4px;
background-color: $primary-bg-color;
padding-right: 5px;
}
.mx_RoomHeader_info {
@ -197,7 +197,7 @@ limitations under the License.
}
.mx_RoomHeader_button {
margin-left: 12px;
margin-left: 10px;
cursor: pointer;
}

View File

@ -0,0 +1,44 @@
/*
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_RoomRecoveryReminder {
display: flex;
flex-direction: column;
text-align: center;
background-color: $room-warning-bg-color;
padding: 20px;
border: 1px solid $primary-hairline-color;
border-bottom: unset;
}
.mx_RoomRecoveryReminder_header {
font-weight: bold;
margin-bottom: 1em;
}
.mx_RoomRecoveryReminder_body {
margin-bottom: 1em;
}
.mx_RoomRecoveryReminder_button {
@mixin mx_DialogButton;
margin: 0 10px;
}
.mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary {
@mixin mx_DialogButton_secondary;
background-color: transparent;
}

View File

@ -19,7 +19,7 @@ limitations under the License.
flex-direction: row;
align-items: center;
cursor: pointer;
height: 40px;
height: 34px;
margin: 0;
padding: 0 8px 0 10px;
position: relative;
@ -39,10 +39,6 @@ limitations under the License.
.mx_RoomTile_menuButton {
display: block;
}
.mx_RoomTile_badge {
display: none;
}
}
.mx_RoomTile_tooltip {
@ -52,17 +48,48 @@ limitations under the License.
left: -12px;
}
.mx_RoomTile_avatar {
flex: 0;
padding: 4px;
width: 32px;
height: 32px;
.mx_RoomTile_nameContainer {
display: flex;
align-items: center;
flex: 1;
vertical-align: middle;
}
.mx_RoomTile_labelContainer {
display: flex;
flex-direction: column;
flex: 1;
}
.mx_RoomTile_subtext {
display: inline-block;
font-size: 11px;
padding: 0 0 0 7px;
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
position: relative;
bottom: 4px;
}
.mx_RoomTile_avatar_container {
position: relative;
}
.mx_RoomTile_avatar {
flex: 0;
padding: 4px;
width: 24px;
vertical-align: middle;
}
.mx_RoomTile_hasSubtext .mx_RoomTile_avatar {
padding-top: 0;
vertical-align: super;
}
.mx_RoomTile_dm {
display: block;
position: absolute;
@ -75,7 +102,7 @@ limitations under the License.
flex: 1 5 auto;
font-size: 14px;
font-weight: 600;
padding: 6px;
padding: 0 6px;
color: $roomtile-name-color;
white-space: nowrap;
overflow-x: hidden;
@ -118,7 +145,7 @@ limitations under the License.
}
.mx_RoomTile_unreadNotify .mx_RoomTile_badge {
background-color: $accent-color;
background-color: $roomtile-name-color;
}
.mx_RoomTile_highlight .mx_RoomTile_badge {
@ -136,10 +163,6 @@ limitations under the License.
.mx_RoomTile_selected {
border-radius: 4px;
background-color: $roomtile-selected-bg-color;
.mx_RoomTile_name {
color: $roomtile-selected-color;
}
}
.mx_DNDRoomTile {
@ -168,4 +191,3 @@ limitations under the License.
.mx_RoomTile.mx_RoomTile_transparent:focus {
background-color: $roomtile-transparent-focused-color;
}

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
@charset "utf-8";
.mx_TopUnreadMessagesBar {
z-index: 1000;
position: absolute;
@ -22,6 +24,22 @@ limitations under the License.
width: 38px;
}
.mx_TopUnreadMessagesBar:after {
content: "·";
position: absolute;
top: -8px;
left: 11px;
width: 16px;
height: 16px;
border-radius: 16px;
font-weight: 600;
font-size: 30px;
line-height: 14px;
text-align: center;
color: $secondary-accent-color;
background-color: $accent-color;
}
.mx_TopUnreadMessagesBar_scrollUp {
height: 38px;
border-radius: 19px;

View File

@ -0,0 +1 @@
<svg height="42" width="37" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><mask id="a" fill="#fff"><path d="m23.521 14.596h-1.777a.454.454 0 0 1 -.456-.45v-4.14a8.974 8.974 0 0 0 -8.57-9 8.884 8.884 0 0 0 -9.253 8.82v4.365a.454.454 0 0 1 -.456.45h-1.78a1.218 1.218 0 0 0 -1.229 1.215v15.93a1.218 1.218 0 0 0 1.229 1.214h22.247a1.218 1.218 0 0 0 1.231-1.215v-15.974a1.153 1.153 0 0 0 -1.186-1.215zm-17.276-4.77a6.114 6.114 0 0 1 6.473-6.075 6.251 6.251 0 0 1 5.88 6.255v4.185a.454.454 0 0 1 -.456.45h-11.486a.454.454 0 0 1 -.456-.45v-4.365zm20.255 11.174c6.344.019 11.481 5.156 11.5 11.5 0 6.351-5.149 11.5-11.5 11.5s-11.5-5.149-11.5-11.5 5.149-11.5 11.5-11.5z" fill="#fff" fill-rule="evenodd"/></mask><g fill="#000" fill-rule="evenodd"><path d="m-.909 32.909h19.773c2.392-6.604 4.34-10.526 5.844-11.766s1.808-8.258.912-21.052h-26.529z" mask="url(#a)" transform="translate(0 -1)"/><path d="m26.5 21c-5.799 0-10.5 4.701-10.5 10.5s4.701 10.5 10.5 10.5 10.5-4.701 10.5-10.5c-.017-5.792-4.708-10.483-10.5-10.5zm1.444 16.012h-2.888v-2.493h3.019v2.494zm.131-9.712-.787 5.775h-1.575l-.788-5.775v-1.312h3.15z" fill-rule="nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,14 @@
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="LifeBuoy" transform="translate(-1073.000000, -872.000000)">
<g id="face-copy" transform="translate(1074.000000, 873.000000)">
<circle id="Oval" stroke="#B8BEC9" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" cx="8" cy="8" r="8"></circle>
<path d="M7.6,7.6 C8.13333333,8.71262059 8.4,9.77928725 8.4,10.8 C8.4,11.8207127 8.13333333,12.8873794 7.6,14" id="Path" stroke="#B8BEC9" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" transform="translate(8.000000, 10.800000) rotate(90.000000) translate(-8.000000, -10.800000) "></path>
<path d="" id="Path-Copy" stroke="#B8BEC9" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" transform="translate(4.800000, 4.800000) rotate(90.000000) translate(-4.800000, -4.800000) "></path>
<circle id="Oval" fill="#B8BEC9" fill-rule="nonzero" cx="4.8" cy="5.6" r="1"></circle>
<circle id="Oval-Copy" fill="#B8BEC9" fill-rule="nonzero" cx="11.2" cy="5.6" r="1"></circle>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,11 @@
<svg width="15px" height="18px" viewBox="0 0 15 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1433.000000, -90.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="file-copy" transform="translate(1434.000000, 91.000000)">
<path d="M7.3125,0 L1.625,0 C0.727537282,0 0,0.7163444 0,1.6 L0,14.4 C0,15.2836556 0.727537282,16 1.625,16 L11.375,16 C12.2724627,16 13,15.2836556 13,14.4 L13,5.6 L7.3125,0 Z" id="Path"></path>
<polyline id="Path" points="7.3125 0 7.3125 5.6 13 5.6"></polyline>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 803 B

View File

@ -0,0 +1,13 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1350.000000, -91.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="grid-copy" transform="translate(1351.000000, 92.000000)">
<rect id="Rectangle" x="0" y="0" width="5.44444444" height="5.44444444"></rect>
<rect id="Rectangle" x="8.55555556" y="0" width="5.44444444" height="5.44444444"></rect>
<rect id="Rectangle" x="8.55555556" y="8.55555556" width="5.44444444" height="5.44444444"></rect>
<rect id="Rectangle" x="0" y="8.55555556" width="5.44444444" height="5.44444444"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 929 B

View File

@ -0,0 +1,18 @@
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-77.000000, -862.000000)" stroke="#2E3648" stroke-width="1.6">
<g id="Buoy" transform="translate(68.000000, 853.000000)">
<g id="life-buoy" transform="translate(10.000000, 10.000000)">
<circle id="Oval" cx="10" cy="10" r="10"></circle>
<circle id="Oval" cx="10" cy="10" r="4"></circle>
<path d="M2.93,2.93 L7.17,7.17" id="Path"></path>
<path d="M12.83,12.83 L17.07,17.07" id="Path"></path>
<path d="M12.83,7.17 L17.07,2.93" id="Path"></path>
<path d="M12.83,7.17 L16.36,3.64" id="Path"></path>
<path d="M2.93,17.07 L7.17,12.83" id="Path"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,10 @@
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1460.000000, -90.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="bell-copy" transform="translate(1461.000000, 91.000000)">
<path d="M16,12 L0,12 C1.3254834,12 2.4,10.9254834 2.4,9.6 L2.4,5.6 C2.40000005,2.50720543 4.90720543,8.34465016e-08 8,8.3446502e-08 C11.0927946,8.34465023e-08 13.6,2.50720543 13.6,5.6 L13.6,9.6 C13.6,10.9254834 14.6745166,12 16,12 Z M9.384,15.2 C9.09776179,15.6934435 8.57045489,15.997165 8,15.997165 C7.42954511,15.997165 6.90223821,15.6934435 6.616,15.2 L9.384,15.2 Z" id="Shape"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View File

@ -0,0 +1,10 @@
<svg width="18px" height="19px" viewBox="0 0 18 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1103.000000, -871.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="paperclip-copy" transform="translate(1104.000000, 872.000000)">
<path d="M15.552,8.13571429 L8.2,15.5752381 C6.32444099,17.4731252 3.28355901,17.4731252 1.408,15.5752381 C-0.46755901,13.677351 -0.46755901,10.600268 1.408,8.70238095 L8.76,1.26285714 C10.0103727,-0.00240088755 12.0376273,-0.00240087226 13.288,1.26285718 C14.5383726,2.52811523 14.5383726,4.57950384 13.288,5.8447619 L5.928,13.2842857 C5.30281366,13.9169147 4.28918634,13.9169147 3.664,13.2842857 C3.03881366,12.6516567 3.03881366,11.6259624 3.664,10.9933333 L10.456,4.12857143" id="Path"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,10 @@
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1133.000000, -872.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="phone-copy" transform="translate(1134.000000, 873.000000)">
<path d="M16.1904762,11.936 L16.1904762,14.336 C16.1923266,14.7865161 16.001896,15.2169267 15.6659678,15.5214919 C15.3300396,15.8260571 14.879747,15.9765505 14.4257143,15.936 C11.934675,15.6685126 9.54185647,14.8273156 7.43952381,13.48 C5.48357515,12.2517311 3.82527212,10.6129375 2.58238095,8.68 C1.21426795,6.59296319 0.362863382,4.21679565 0.0971428571,1.744 C0.0562402179,1.29669787 0.207458292,0.853001939 0.513858012,0.521296845 C0.820257732,0.18959175 1.25362391,0.000422952191 1.70809524,0 L4.13666667,0 C4.94931852,-0.00790412572 5.64197687,0.580773986 5.75571429,1.376 C5.85821863,2.14405316 6.04831623,2.89818151 6.32238095,3.624 C6.54478548,4.20870002 6.40253601,4.86784501 5.95809524,5.312 L4.93,6.328 C6.08240205,8.33083666 7.7604629,9.98915562 9.78714286,11.128 L10.8152381,10.112 C11.2646806,9.67278794 11.9316726,9.532212 12.5233333,9.752 C13.2577925,10.0228404 14.0208986,10.2107016 14.7980952,10.312 C15.6121222,10.4254883 16.2108681,11.1238341 16.1904762,11.936 Z" id="Path"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,11 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1378.000000, -91.000000)" stroke="#61708b" stroke-width="1.6">
<g id="search-copy" transform="translate(1379.000000, 92.000000)">
<circle id="Oval" cx="6.22222222" cy="6.22222222" r="6.22222222"></circle>
<path d="M14,14 L10.6166667,10.6166667" id="Path"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View File

@ -0,0 +1,11 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1378.000000, -91.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="search-copy" transform="translate(1379.000000, 92.000000)">
<circle id="Oval" cx="6.22222222" cy="6.22222222" r="6.22222222"></circle>
<path d="M14,14 L10.6166667,10.6166667" id="Path"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View File

@ -0,0 +1,11 @@
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1290.000000, -89.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="settings-copy" transform="translate(1291.000000, 90.000000)">
<circle id="Oval" cx="9" cy="9" r="2.45454545"></circle>
<path d="M15.0545455,11.4545455 C14.8317165,11.9594373 14.938637,12.5491199 15.3245455,12.9436364 L15.3736364,12.9927273 C15.6809079,13.2996571 15.85356,13.7161485 15.85356,14.1504545 C15.85356,14.5847606 15.6809079,15.0012519 15.3736364,15.3081818 C15.0667065,15.6154533 14.6502151,15.7881054 14.2159091,15.7881054 C13.781603,15.7881054 13.3651117,15.6154533 13.0581818,15.3081818 L13.0090909,15.2590909 C12.6145745,14.8731825 12.0248919,14.766262 11.52,14.9890909 C11.0254331,15.2010559 10.7039642,15.686474 10.7018182,16.2245455 L10.7018182,16.3636364 C10.7018182,17.267375 9.96919323,18 9.06545455,18 C8.16171586,18 7.42909091,17.267375 7.42909091,16.3636364 L7.42909091,16.29 C7.41612813,15.7358216 7.06571327,15.2458897 6.54545455,15.0545455 C6.04056267,14.8317165 5.45088006,14.938637 5.05636364,15.3245455 L5.00727273,15.3736364 C4.70034285,15.6809079 4.2838515,15.85356 3.84954545,15.85356 C3.41523941,15.85356 2.99874806,15.6809079 2.69181818,15.3736364 C2.38454666,15.0667065 2.21189456,14.6502151 2.21189456,14.2159091 C2.21189456,13.781603 2.38454666,13.3651117 2.69181818,13.0581818 L2.74090909,13.0090909 C3.12681754,12.6145745 3.23373801,12.0248919 3.01090909,11.52 C2.79894413,11.0254331 2.31352603,10.7039642 1.77545455,10.7018182 L1.63636364,10.7018182 C0.732624955,10.7018182 1.81672859e-16,9.96919323 0,9.06545455 C-9.08364293e-17,8.16171586 0.732624955,7.42909091 1.63636364,7.42909091 L1.71,7.42909091 C2.26417842,7.41612813 2.75411031,7.06571327 2.94545455,6.54545455 C3.16828346,6.04056267 3.06136299,5.45088006 2.67545455,5.05636364 L2.62636364,5.00727273 C2.31909211,4.70034285 2.14644002,4.2838515 2.14644002,3.84954545 C2.14644002,3.41523941 2.31909211,2.99874806 2.62636364,2.69181818 C2.93329351,2.38454666 3.34978487,2.21189456 3.78409091,2.21189456 C4.21839695,2.21189456 4.63488831,2.38454666 4.94181818,2.69181818 L4.99090909,2.74090909 C5.38542551,3.12681754 5.97510812,3.23373801 6.48,3.01090909 L6.54545455,3.01090909 C7.04002141,2.79894413 7.36149035,2.31352603 7.36363636,1.77545455 L7.36363636,1.63636364 C7.36363636,0.732624955 8.09626132,1.81672859e-16 9,0 C9.90373868,0 10.6363636,0.732624955 10.6363636,1.63636364 L10.6363636,1.71 C10.6385096,2.24807148 10.9599786,2.73348959 11.4545455,2.94545455 C11.9594373,3.16828346 12.5491199,3.06136299 12.9436364,2.67545455 L12.9927273,2.62636364 C13.2996571,2.31909211 13.7161485,2.14644002 14.1504545,2.14644002 C14.5847606,2.14644002 15.0012519,2.31909211 15.3081818,2.62636364 C15.6154533,2.93329351 15.7881054,3.34978487 15.7881054,3.78409091 C15.7881054,4.21839695 15.6154533,4.63488831 15.3081818,4.94181818 L15.2590909,4.99090909 C14.8731825,5.38542551 14.766262,5.97510812 14.9890909,6.48 L14.9890909,6.54545455 C15.2010559,7.04002141 15.686474,7.36149035 16.2245455,7.36363636 L16.3636364,7.36363636 C17.267375,7.36363636 18,8.09626132 18,9 C18,9.90373868 17.267375,10.6363636 16.3636364,10.6363636 L16.29,10.6363636 C15.7519285,10.6385096 15.2665104,10.9599786 15.0545455,11.4545455 Z" id="Path"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,14 @@
<svg width="16px" height="18px" viewBox="0 0 16 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1322.000000, -90.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="share-2-copy" transform="translate(1323.000000, 91.000000)">
<ellipse id="Oval" cx="11.6666667" cy="2.4" rx="2.33333333" ry="2.4"></ellipse>
<ellipse id="Oval" cx="2.33333333" cy="8" rx="2.33333333" ry="2.4"></ellipse>
<ellipse id="Oval" cx="11.6666667" cy="13.6" rx="2.33333333" ry="2.4"></ellipse>
<path d="M4.34777778,9.208 L9.66,12.392" id="Path"></path>
<path d="M9.65222222,3.608 L4.34777778,6.792" id="Path"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 954 B

View File

@ -0,0 +1,13 @@
<svg width="20px" height="16px" viewBox="0 0 20 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1269.000000, -143.000000)" stroke="#FFFFFF" stroke-width="1.6">
<g id="user-plus-copy" transform="translate(1270.000000, 144.000000)">
<path d="M12.2727273,14 L12.2727273,12.4444444 C12.2727273,10.7262252 10.8074774,9.33333333 9,9.33333333 L3.27272727,9.33333333 C1.46524991,9.33333333 1.81672859e-16,10.7262252 0,12.4444444 L0,14" id="Path"></path>
<ellipse id="Oval" cx="6.13636364" cy="3.11111111" rx="3.27272727" ry="3.11111111"></ellipse>
<path d="M15.5454545,3.88888889 L15.5454545,8.55555556" id="Path"></path>
<path d="M18,6.22222222 L13.0909091,6.22222222" id="Path"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,11 @@
<svg width="15px" height="16px" viewBox="0 0 15 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1406.000000, -91.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="user-copy" transform="translate(1407.000000, 92.000000)">
<path d="M13,14 L13,12.4444444 C13,10.7262252 11.5449254,9.33333333 9.75,9.33333333 L3.25,9.33333333 C1.45507456,9.33333333 0,10.7262252 0,12.4444444 L0,14" id="Path"></path>
<ellipse id="Oval" cx="6.5" cy="3.11111111" rx="3.25" ry="3.11111111"></ellipse>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@ -0,0 +1,15 @@
<svg width="20px" height="16px" viewBox="0 0 20 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy-Copy" transform="translate(-78.000000, -813.000000)" stroke="#293042" stroke-width="1.6">
<g id="Buoy-Copy" transform="translate(68.000000, 802.000000)">
<g id="users-copy" transform="translate(11.000000, 12.000000)">
<path d="M13.0909091,14 L13.0909091,12.4444444 C13.0909091,10.7262252 11.6256592,9.33333333 9.81818182,9.33333333 L3.27272727,9.33333333 C1.46524991,9.33333333 1.81672859e-16,10.7262252 0,12.4444444 L0,14" id="Path"></path>
<ellipse id="Oval" cx="6.54545455" cy="3.11111111" rx="3.27272727" ry="3.11111111"></ellipse>
<path d="M18,14 L18,12.4444444 C17.9988875,11.0266471 16.9895445,9.78889348 15.5454545,9.43444444" id="Path"></path>
<path d="M12.2727273,0.101111111 C13.7208394,0.453576501 14.7336899,1.69399311 14.7336899,3.115 C14.7336899,4.53600689 13.7208394,5.7764235 12.2727273,6.12888889" id="Path"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,11 @@
<svg width="20px" height="13px" viewBox="0 0 20 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1164.000000, -874.000000)" stroke="#B8BEC9" stroke-width="1.6">
<g id="video-copy" transform="translate(1165.000000, 875.000000)">
<polygon id="Path" points="18 1.57142857 12.2727273 5.5 18 9.42857143"></polygon>
<rect id="Rectangle" x="0" y="0" width="12.2727273" height="11" rx="1.6"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 707 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
<title>Tick</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Custom-Status-Copy" transform="translate(-529.000000, -917.000000)" fill-rule="nonzero">
<g id="Tick" transform="translate(530.000000, 918.000000)">
<circle id="Oval" stroke="#6AAC8C" fill="#75CFA6" cx="9" cy="9" r="9"></circle>
<g id="Glyph" transform="translate(8.949747, 7.949747) rotate(-45.000000) translate(-8.949747, -7.949747) translate(4.449747, 5.449747)" fill="#FFFFFF">
<rect id="Rectangle" x="0" y="0" width="2" height="5"></rect>
<rect id="Rectangle" x="0" y="3" width="9" height="2"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -75,22 +75,22 @@
<g
id="icons_create_room"
transform="translate(20,18)">
<path
id="Line"
class="st1"
d="M -2.5,28.5 4.6,21.4"
style="fill:none;stroke:#9fa9ba;stroke-width:2;stroke-linecap:round;stroke-opacity:1"
style="fill:none;stroke:#454545;stroke-width:2;stroke-linecap:round;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
id="Line_1_"
class="st1"
d="m -2.5,21.5 7.1,7.1"
style="fill:none;stroke:#9fa9ba;stroke-width:2;stroke-linecap:round;stroke-opacity:1"
style="fill:none;stroke:#454545;stroke-width:2;stroke-linecap:round;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,71 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="11.521363"
height="11.521363"
viewBox="0 0 11.521363 11.521363"
version="1.1"
id="svg4"
sodipodi:docname="icons-room-add.svg"
style="fill:none"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1586"
inkscape:window-height="988"
id="namedview6"
showgrid="false"
fit-margin-top="2"
fit-margin-left="2"
fit-margin-right="2"
fit-margin-bottom="2"
inkscape:zoom="29.5"
inkscape:cx="5.8284785"
inkscape:cy="5.7606831"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="m 5.7606819,2.7606819 v 6"
id="path2"
inkscape:connector-curvature="0"
style="stroke:#ffffff;stroke-width:1.5;stroke-linecap:round" />
<g
style="fill:none"
id="g876"
transform="translate(1.7606819,4.7606819)">
<path
id="path865"
d="M 7,1 H 1"
inkscape:connector-curvature="0"
style="stroke:#ffffff;stroke-width:1.5;stroke-linecap:round" />
</g>
<svg width="8px" height="8px" viewBox="0 0 8 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Design" stroke="none" stroke-width="1.6" fill="none" fill-rule="evenodd" stroke-linecap="round">
<g id="Add" transform="translate(1.000000, 1.000000)" fill-rule="nonzero" stroke="#FFFFFF">
<path d="M3,0 L3,6" id="Stroke H"></path>
<path d="M3,0 L3,6" id="Stroke V" transform="translate(3.000000, 3.000000) rotate(90.000000) translate(-3.000000, -3.000000) "></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 580 B

View File

@ -64,7 +64,7 @@
<g
id="matrix-my-stuff-no-lines-message-context-menu-smaller-icons"
transform="translate(-203,-25)"
style="stroke:#212121;stroke-width:1.29999995">
style="stroke:#61708b;stroke-width:1.6">
<g
id="Group-3"
transform="translate(128,15)">

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -12,6 +12,7 @@ $light-fg-color: #747474;
// button UI (white-on-green in light skin)
$accent-fg-color: $primary-bg-color;
$accent-color: #76CFA6;
$accent-color-alt: $accent-color;
$accent-color-50pct: #76CFA67F;
$selection-fg-color: $primary-fg-color;
@ -106,6 +107,8 @@ $voip-accept-color: #80f480;
$rte-bg-color: #353535;
$rte-code-bg-color: #000;
$room-warning-bg-color: #2d2d2d;
// ********************
$roomtile-name-color: rgba(186, 186, 186, 0.8);
@ -184,6 +187,14 @@ $progressbar-color: #000;
outline: none;
}
@define-mixin mx_DialogButton_secondary {
// flip colours for the secondary ones
font-weight: 600;
border: 1px solid $accent-color ! important;
color: $accent-color;
background-color: $accent-fg-color;
}
// Nasty hacks to apply a filter to arbitrary monochrome artwork to make it
// better match the theme. Typically applied to dark grey 'off' buttons or
// light grey 'on' buttons.

View File

@ -20,19 +20,20 @@ $focus-bg-color: #dddddd;
// button UI (white-on-green in light skin)
$accent-fg-color: #ffffff;
$accent-color: #f56679;
$accent-color-50pct: #f56679;
$accent-color: #7ac9a1;
$accent-color-50pct: #92caad;
$accent-color-alt: #238CF5;
$selection-fg-color: $primary-bg-color;
$focus-brightness: 125%;
$focus-brightness: 105%;
// red warning colour
$warning-color: #ff0064;
$warning-color: #f56679;
// background colour for warnings
$warning-bg-color: #DF2A8B;
$info-bg-color: #2A9EDF;
$mention-user-pill-bg-color: #ff0064;
$mention-user-pill-bg-color: $warning-color;
$other-user-pill-bg-color: rgba(0, 0, 0, 0.1);
// pinned events indicator
@ -121,13 +122,13 @@ $rte-group-pill-color: #aaa;
$topleftmenu-color: #212121;
$roomheader-color: #45474a;
$roomheader-addroom-color: #929eb4;
$roomheader-addroom-color: #91A1C0;
$roomtopic-color: #9fa9ba;
$eventtile-meta-color: $roomtopic-color;
// ********************
$roomtile-name-color: #929eb4;
$roomtile-name-color: #61708b;
$roomtile-selected-color: #212121;
$roomtile-notified-color: #212121;
$roomtile-selected-bg-color: #fff;
@ -185,6 +186,8 @@ $lightbox-border-color: #ffffff;
// unused?
$progressbar-color: #000;
$room-warning-bg-color: #fff8e3;
/*** form elements ***/
// .mx_textinput is a container for a text input
@ -192,32 +195,40 @@ $progressbar-color: #000;
// it has the appearance of a text box so the controls
// appear to be part of the input
:not(.mx_textinput) > input[type=text],
:not(.mx_textinput) > input[type=search],
.mx_textinput {
display: block;
margin: 9px;
box-sizing: border-box;
background-color: transparent;
color: $input-darker-fg-color;
border-radius: 4px;
border: 1px solid #c1c1c1;
}
.mx_MatrixChat {
.mx_textinput {
display: flex;
align-items: center;
}
:not(.mx_textinput) > input[type=text],
:not(.mx_textinput) > input[type=search],
.mx_textinput {
display: block;
margin: 9px;
box-sizing: border-box;
background-color: transparent;
color: $input-darker-fg-color;
border-radius: 4px;
border: 1px solid #c1c1c1;
flex: 0 0 auto;
}
.mx_textinput > input[type=text],
.mx_textinput > input[type=search] {
border: none;
flex: 1;
color: inherit; //from .mx_textinput
.mx_textinput {
display: flex;
align-items: center;
> input[type=text],
> input[type=search] {
border: none;
flex: 1;
color: $primary-fg-color;
},
input::placeholder {
color: $roomsublist-label-fg-color;
}
}
}
input[type=text],
input[type=search] {
input[type=search],
input[type=password] {
padding: 9px;
font-family: $font-family;
font-size: 14px;
@ -257,9 +268,11 @@ input[type=search].mx_textinput_icon {
background-position: 10px center;
}
// FIXME THEME - Tint by CSS rather than referencing a duplicate asset
input[type=text].mx_textinput_icon.mx_textinput_search,
input[type=search].mx_textinput_icon.mx_textinput_search {
background-image: url('../../img/icons-search.svg');
background-image: url('../../img/feather-icons/search-input.svg');
}
// dont search UI as not all browsers support it,
@ -309,3 +322,11 @@ input[type=search]::-webkit-search-results-decoration {
font-size: 15px;
padding: 0px 1.5em 0px 1.5em;
}
@define-mixin mx_DialogButton_secondary {
// flip colours for the secondary ones
font-weight: 600;
border: 1px solid $accent-color ! important;
color: $accent-color;
background-color: $accent-fg-color;
}

View File

@ -2,7 +2,7 @@
* Nunito.
* Includes extended Latin and Vietnamese character sets
* Current URLs are v9, derived from the contents of
* https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&amp;subset=latin-ext,vietnamese
* https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&subset=latin-ext,vietnamese
*/
/* the 'src' links are relative to the bundle.css, which is in a subdirectory.
@ -11,37 +11,37 @@
font-family: 'Nunito';
font-style: italic;
font-weight: 400;
src: local('Nunito Italic'), local('Nunito-Italic'), url('../../fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf') format('truetype');
src: url('../../fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 600;
src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf') format('truetype');
src: url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 700;
src: local('Nunito Bold Italic'), local('Nunito-BoldItalic'), url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf') format('truetype');
src: url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 400;
src: local('Nunito Regular'), local('Nunito-Regular'), url('../../fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf') format('truetype');
src: url('../../fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 600;
src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url('../../fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf') format('truetype');
src: url('../../fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 700;
src: local('Nunito Bold'), local('Nunito-Bold'), url('../../fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf') format('truetype');
src: url('../../fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf') format('truetype');
}
/*

View File

@ -20,6 +20,7 @@ $focus-bg-color: #dddddd;
// button UI (white-on-green in light skin)
$accent-fg-color: #ffffff;
$accent-color: #76CFA6;
$accent-color-alt: $accent-color;
$accent-color-50pct: #76CFA67F;
$selection-fg-color: $primary-bg-color;
@ -180,6 +181,8 @@ $imagebody-giflabel-border: rgba(0, 0, 0, 0.2);
// unused?
$progressbar-color: #000;
$room-warning-bg-color: #fff8e3;
// ***** Mixins! *****
@define-mixin mx_DialogButton {
@ -212,3 +215,11 @@ $progressbar-color: #000;
font-size: 15px;
padding: 0px 1.5em 0px 1.5em;
}
@define-mixin mx_DialogButton_secondary {
// flip colours for the secondary ones
font-weight: 600;
border: 1px solid $accent-color ! important;
color: $accent-color;
background-color: $accent-fg-color;
}

View File

@ -1,22 +1,27 @@
#!/bin/sh
set -e
org="$1"
repo="$2"
defbranch="$3"
[ -z "$defbranch" ] && defbranch="develop"
rm -r "$repo" || true
curbranch="$TRAVIS_PULL_REQUEST_BRANCH"
[ -z "$curbranch" ] && curbranch="$TRAVIS_BRANCH"
[ -z "$curbranch" ] && curbranch=`"echo $GIT_BRANCH" | sed -e 's/^origin\///'` # jenkins
clone() {
branch=$1
if [ -n "$branch" ]
then
echo "Trying to use the branch $branch"
git clone https://github.com/$org/$repo.git $repo --branch "$branch" && exit 0
fi
}
if [ -n "$curbranch" ]
then
echo "Determined branch to be $curbranch"
git clone https://github.com/$org/$repo.git $repo --branch "$curbranch" && exit 0
fi
echo "Checking out develop branch"
git clone https://github.com/$org/$repo.git $repo --branch develop
# Try the PR author's branch in case it exists on the deps as well.
clone $TRAVIS_PULL_REQUEST_BRANCH
# Try the target branch of the push or PR.
clone $TRAVIS_BRANCH
# Try the current branch from Jenkins.
clone `"echo $GIT_BRANCH" | sed -e 's/^origin\///'`
# Use the default branch as the last resort.
clone $defbranch

View File

@ -222,10 +222,21 @@ const translatables = new Set();
const walkOpts = {
listeners: {
names: function(root, nodeNamesArray) {
// Sort the names case insensitively and alphabetically to
// maintain some sense of order between the different strings.
nodeNamesArray.sort((a, b) => {
a = a.toLowerCase();
b = b.toLowerCase();
if (a > b) return 1;
if (a < b) return -1;
return 0;
});
},
file: function(root, fileStats, next) {
const fullPath = path.join(root, fileStats.name);
let ltrs;
let trs;
if (fileStats.name.endsWith('.js')) {
trs = getTranslationsJs(fullPath);
} else if (fileStats.name.endsWith('.html')) {
@ -235,7 +246,8 @@ const walkOpts = {
}
console.log(`${fullPath} (${trs.size} strings)`);
for (const tr of trs.values()) {
translatables.add(tr);
// Convert DOS line endings to unix
translatables.add(tr.replace(/\r\n/g, "\n"));
}
},
}

View File

@ -3,6 +3,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 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.
@ -105,11 +106,6 @@ export default class BasePlatform {
return "Not implemented";
}
isElectron(): boolean { return false; }
setupScreenSharingForIframe() {
}
/**
* Restarts the application, without neccessarily reloading
* any application code

View File

@ -377,9 +377,9 @@ class ContentMessages {
}
}
if (error) {
dis.dispatch({action: 'upload_failed', upload: upload});
dis.dispatch({action: 'upload_failed', upload, error});
} else {
dis.dispatch({action: 'upload_finished', upload: upload});
dis.dispatch({action: 'upload_finished', upload});
}
});
}

View File

@ -32,6 +32,7 @@ import Modal from './Modal';
import sdk from './index';
import ActiveWidgetStore from './stores/ActiveWidgetStore';
import PlatformPeg from "./PlatformPeg";
import {sendLoginRequest} from "./Login";
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -129,27 +130,17 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
return Promise.resolve(false);
}
// create a temporary MatrixClient to do the login
const client = Matrix.createClient({
baseUrl: queryParams.homeserver,
});
return client.login(
return sendLoginRequest(
queryParams.homeserver,
queryParams.identityServer,
"m.login.token", {
token: queryParams.loginToken,
initial_device_display_name: defaultDeviceDisplayName,
},
).then(function(data) {
).then(function(creds) {
console.log("Logged in with token");
return _clearStorage().then(() => {
_persistCredentialsToLocalStorage({
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
homeserverUrl: queryParams.homeserver,
identityServerUrl: queryParams.identityServer,
guest: false,
});
_persistCredentialsToLocalStorage(creds);
return true;
});
}).catch((err) => {
@ -506,16 +497,7 @@ function _clearStorage() {
Analytics.logout();
if (window.localStorage) {
const hsUrl = window.localStorage.getItem("mx_hs_url");
const isUrl = window.localStorage.getItem("mx_is_url");
window.localStorage.clear();
// preserve our HS & IS URLs for convenience
// N.B. we cache them in hsUrl/isUrl and can't really inline them
// as getCurrentHsUrl() may call through to localStorage.
// NB. We do clear the device ID (as well as all the settings)
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
}
// create a temporary client to clear out the persistent stores.

View File

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@ -17,7 +18,6 @@ limitations under the License.
import Matrix from "matrix-js-sdk";
import Promise from 'bluebird';
import url from 'url';
export default class Login {
@ -141,60 +141,20 @@ export default class Login {
};
Object.assign(loginParams, legacyParams);
const client = this._createTemporaryClient();
const tryFallbackHs = (originalError) => {
const fbClient = Matrix.createClient({
baseUrl: self._fallbackHsUrl,
idBaseUrl: this._isUrl,
});
return fbClient.login('m.login.password', loginParams).then(function(data) {
return Promise.resolve({
homeserverUrl: self._fallbackHsUrl,
identityServerUrl: self._isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
});
}).catch((fallback_error) => {
return sendLoginRequest(
self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams,
).catch((fallback_error) => {
console.log("fallback HS login failed", fallback_error);
// throw the original error
throw originalError;
});
};
const tryLowercaseUsername = (originalError) => {
const loginParamsLowercase = Object.assign({}, loginParams, {
user: username.toLowerCase(),
identifier: {
user: username.toLowerCase(),
},
});
return client.login('m.login.password', loginParamsLowercase).then(function(data) {
return Promise.resolve({
homeserverUrl: self._hsUrl,
identityServerUrl: self._isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
});
}).catch((fallback_error) => {
console.log("Lowercase username login failed", fallback_error);
// throw the original error
throw originalError;
});
};
let originalLoginError = null;
return client.login('m.login.password', loginParams).then(function(data) {
return Promise.resolve({
homeserverUrl: self._hsUrl,
identityServerUrl: self._isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
});
}).catch((error) => {
return sendLoginRequest(
self._hsUrl, self._isUrl, 'm.login.password', loginParams,
).catch((error) => {
originalLoginError = error;
if (error.httpStatus === 403) {
if (self._fallbackHsUrl) {
@ -202,22 +162,6 @@ export default class Login {
}
}
throw originalLoginError;
}).catch((error) => {
// We apparently squash case at login serverside these days:
// https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475
// so this wasn't needed after all. Keeping the code around in case the
// the situation changes...
/*
if (
error.httpStatus === 403 &&
loginParams.identifier.type === 'm.id.user' &&
username.search(/[A-Z]/) > -1
) {
return tryLowercaseUsername(originalLoginError);
}
*/
throw originalLoginError;
}).catch((error) => {
console.log("Login failed", error);
throw error;
@ -239,3 +183,45 @@ export default class Login {
return client.getSsoLoginUrl(url.format(parsedUrl), loginType);
}
}
/**
* Send a login request to the given server, and format the response
* as a MatrixClientCreds
*
* @param {string} hsUrl the base url of the Homeserver used to log in.
* @param {string} isUrl the base url of the default identity server
* @param {string} loginType the type of login to do
* @param {object} loginParams the parameters for the login
*
* @returns {MatrixClientCreds}
*/
export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
const client = Matrix.createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
});
const data = await client.login(loginType, loginParams);
const wellknown = data.well_known;
if (wellknown) {
if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) {
hsUrl = wellknown["m.homeserver"]["base_url"];
console.log(`Overrode homeserver setting with ${hsUrl} from login response`);
}
if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) {
// TODO: should we prompt here?
isUrl = wellknown["m.identity_server"]["base_url"];
console.log(`Overrode IS setting with ${isUrl} from login response`);
}
}
return {
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
};
}

View File

@ -26,6 +26,10 @@ import MatrixClientPeg from './MatrixClientPeg';
import Modal from './Modal';
import { _t } from './languageHandler';
// Regex for what a "safe" or "Matrix-looking" localpart would be.
// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514
export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
/**
* Starts either the ILAG or full registration flow, depending
* on what the HS supports

View File

@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 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.
@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import MatrixClientPeg from './MatrixClientPeg';
import MultiInviter from './utils/MultiInviter';
import Modal from './Modal';
@ -25,18 +26,6 @@ import dis from './dispatcher';
import DMRoomMap from './utils/DMRoomMap';
import { _t } from './languageHandler';
export function inviteToRoom(roomId, addr) {
const addrType = getAddressType(addr);
if (addrType == 'email') {
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
} else if (addrType == 'mx-user-id') {
return MatrixClientPeg.get().invite(roomId, addr);
} else {
throw new Error('Unsupported address');
}
}
/**
* Invites multiple addresses to a room
* Simpler interface to utils/MultiInviter but with
@ -46,9 +35,9 @@ export function inviteToRoom(roomId, addr) {
* @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids.
* @returns {Promise} Promise
*/
export function inviteMultipleToRoom(roomId, addrs) {
function inviteMultipleToRoom(roomId, addrs) {
const inviter = new MultiInviter(roomId);
return inviter.invite(addrs);
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
}
export function showStartChatInviteDialog() {
@ -129,8 +118,8 @@ function _onStartChatFinished(shouldInvite, addrs) {
createRoom().then((roomId) => {
room = MatrixClientPeg.get().getRoom(roomId);
return inviteMultipleToRoom(roomId, addrTexts);
}).then((addrs) => {
return _showAnyInviteErrors(addrs, room);
}).then((result) => {
return _showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => {
console.error(err.stack);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -148,9 +137,9 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
const addrTexts = addrs.map((addr) => addr.address);
// Invite new users to a room
inviteMultipleToRoom(roomId, addrTexts).then((addrs) => {
inviteMultipleToRoom(roomId, addrTexts).then((result) => {
const room = MatrixClientPeg.get().getRoom(roomId);
return _showAnyInviteErrors(addrs, room);
return _showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => {
console.error(err.stack);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -169,22 +158,36 @@ function _isDmChat(addrTexts) {
}
}
function _showAnyInviteErrors(addrs, room) {
function _showAnyInviteErrors(addrs, room, inviter) {
// Show user any errors
const errorList = [];
for (const addr of Object.keys(addrs)) {
if (addrs[addr] === "error") {
errorList.push(addr);
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
if (failedUsers.length === 1 && inviter.fatal) {
// Just get the first message because there was a fatal problem on the first
// user. This usually means that no other users were attempted, making it
// pointless for us to list who failed exactly.
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, {
title: _t("Failed to invite users to the room:", {roomName: room.name}),
description: inviter.getErrorText(failedUsers[0]),
});
} else {
const errorList = [];
for (const addr of failedUsers) {
if (addrs[addr] === "error") {
const reason = inviter.getErrorText(addr);
errorList.push(addr + ": " + reason);
}
}
if (errorList.length > 0) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description: errorList.join(<br />),
});
}
}
if (errorList.length > 0) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description: errorList.join(", "),
});
}
return addrs;
}

View File

@ -26,6 +26,7 @@ import Modal from './Modal';
import SettingsStore, {SettingLevel} from './settings/SettingsStore';
import {MATRIXTO_URL_PATTERN} from "./linkify-matrix";
import * as querystring from "querystring";
import MultiInviter from './utils/MultiInviter';
class Command {
@ -134,6 +135,18 @@ export const CommandMap = {
},
}),
roomname: new Command({
name: 'roomname',
args: '<name>',
description: _td('Sets the room name'),
runFn: function(roomId, args) {
if (args) {
return success(MatrixClientPeg.get().setRoomName(roomId, args));
}
return reject(this.getUsage());
},
}),
invite: new Command({
name: 'invite',
args: '<user-id>',
@ -142,7 +155,15 @@ export const CommandMap = {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
return success(MatrixClientPeg.get().invite(roomId, matches[1]));
// We use a MultiInviter to re-use the invite logic, even though
// we're only inviting one user.
const userId = matches[1];
const inviter = new MultiInviter(roomId);
return success(inviter.invite([userId]).then(() => {
if (inviter.getCompletionState(userId) !== "invited") {
throw new Error(inviter.getErrorText(userId));
}
}));
}
}
return reject(this.getUsage());

View File

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg';
import { scorePassword } from '../../../../utils/PasswordScorer';
import FileSaver from 'file-saver';
@ -30,6 +31,8 @@ const PHASE_BACKINGUP = 4;
const PHASE_DONE = 5;
const PHASE_OPTOUT_CONFIRM = 6;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
// XXX: copied from ShareDialog: factor out into utils
function selectText(target) {
const range = document.createRange();
@ -52,6 +55,8 @@ export default React.createClass({
passPhraseConfirm: '',
copied: false,
downloaded: false,
zxcvbnResult: null,
setPassPhrase: false,
};
},
@ -87,25 +92,33 @@ export default React.createClass({
});
},
_createBackup: function() {
_createBackup: async function() {
this.setState({
phase: PHASE_BACKINGUP,
error: null,
});
this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion(
this._keyBackupInfo,
).then((info) => {
return MatrixClientPeg.get().backupAllGroupSessions(info.version);
}).then(() => {
let info;
try {
info = await MatrixClientPeg.get().createKeyBackupVersion(
this._keyBackupInfo,
);
await MatrixClientPeg.get().backupAllGroupSessions(info.version);
this.setState({
phase: PHASE_DONE,
});
}).catch(e => {
} catch (e) {
console.log("Error creating key backup", e);
// TODO: If creating a version succeeds, but backup fails, should we
// delete the version, disable backup, or do nothing? If we just
// disable without deleting, we'll enable on next app reload since
// it is trusted.
if (info) {
MatrixClientPeg.get().deleteKeyBackupVersion(info.version);
}
this.setState({
error: e,
});
});
}
},
_onCancel: function() {
@ -128,6 +141,7 @@ export default React.createClass({
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
this.setState({
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
},
@ -145,7 +159,9 @@ export default React.createClass({
_onPassPhraseConfirmNextClick: async function() {
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
this.setState({
setPassPhrase: true,
copied: false,
downloaded: false,
phase: PHASE_SHOWKEY,
});
},
@ -164,7 +180,7 @@ export default React.createClass({
});
},
_onKeepItSafeGotItClick: function() {
_onKeepItSafeBackClick: function() {
this.setState({
phase: PHASE_SHOWKEY,
});
@ -173,6 +189,10 @@ export default React.createClass({
_onPassPhraseChange: function(e) {
this.setState({
passPhrase: e.target.value,
// precompute this and keep it in state: zxcvbn is fast but
// we use it in a couple of different places so no point recomputing
// it unnecessarily.
zxcvbnResult: scorePassword(e.target.value),
});
},
@ -183,24 +203,55 @@ export default React.createClass({
},
_passPhraseIsValid: function() {
return this.state.passPhrase !== '';
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
},
_renderPhasePassPhrase: function() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let strengthMeter;
let helpText;
if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
helpText = _t("Great! This passphrase looks strong enough.");
} else {
const suggestions = [];
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
}
const suggestionBlock = suggestions.length > 0 ? <div>
{suggestions}
</div> : null;
helpText = <div>
{this.state.zxcvbnResult.feedback.warning}
{suggestionBlock}
</div>;
}
strengthMeter = <div>
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
</div>;
}
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 className="mx_CreateKeyBackupDialog_passPhraseContainer">
<input type="password"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
/>
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
{strengthMeter}
{helpText}
</div>
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
@ -210,7 +261,7 @@ export default React.createClass({
/>
<p>{_t(
"If you don't want encrypted message history to be availble on other devices, "+
"If you don't want encrypted message history to be available on other devices, "+
"<button>opt out</button>.",
{},
{
@ -268,16 +319,18 @@ export default React.createClass({
"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 className="mx_CreateKeyBackupDialog_passPhraseContainer">
<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>
{passPhraseMatch}
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
@ -289,34 +342,34 @@ export default React.createClass({
},
_renderPhaseShowKey: function() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let bodyText;
if (this.state.setPassPhrase) {
bodyText = _t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.");
} else {
bodyText = _t("As a safety net, you can use it to restore your encrypted message history.");
}
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>{bodyText}</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 className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your Recovery Key")}
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
</button>
</div>
</div>
</p>
<br />
<DialogButtons primaryButton={_t("I've made a copy")}
onPrimaryButtonClick={this._createBackup}
hasCancel={false}
disabled={!this.state.copied && !this.state.downloaded}
/>
</div>;
},
@ -341,10 +394,11 @@ export default React.createClass({
<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}
/>
<DialogButtons primaryButton={_t("OK")}
onPrimaryButtonClick={this._createBackup}
hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
</div>;
},

View File

@ -0,0 +1,70 @@
/*
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 PropTypes from "prop-types";
import sdk from "../../../../index";
import { _t } from "../../../../languageHandler";
export default class IgnoreRecoveryReminderDialog extends React.PureComponent {
static propTypes = {
onDontAskAgain: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
onSetup: PropTypes.func.isRequired,
}
onDontAskAgainClick = () => {
this.props.onFinished();
this.props.onDontAskAgain();
}
onSetupClick = () => {
this.props.onFinished();
this.props.onSetup();
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
return (
<BaseDialog className="mx_IgnoreRecoveryReminderDialog"
onFinished={this.props.onFinished}
title={_t("Are you sure?")}
>
<div>
<p>{_t(
"Without setting up Secure Message Recovery, " +
"you'll lose your secure message history when you " +
"log out.",
)}</p>
<p>{_t(
"If you don't want to set this up now, you can later " +
"in Settings.",
)}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("Set up")}
onPrimaryButtonClick={this.onSetupClick}
cancelButton={_t("Don't ask again")}
onCancel={this.onDontAskAgainClick}
/>
</div>
</div>
</BaseDialog>
);
}
}

View File

@ -0,0 +1,110 @@
/*
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 PropTypes from "prop-types";
import sdk from "../../../../index";
import MatrixClientPeg from '../../../../MatrixClientPeg';
import dis from "../../../../dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
export default class NewRecoveryMethodDialog extends React.PureComponent {
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
onGoToSettingsClick = () => {
this.props.onFinished();
dis.dispatch({ action: 'view_user_settings' });
}
onSetupClick = async() => {
// TODO: Should change to a restore key backup flow that checks the
// recovery passphrase while at the same time also cross-signing the
// device as well in a single flow. Since we don't have that yet, we'll
// look for an unverified device and verify it. Note that this means
// we won't restore keys yet; instead we'll only trust the backup for
// sending our own new keys to it.
let backupSigStatus;
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
} catch (e) {
console.log("Unable to fetch key backup status", e);
return;
}
let unverifiedDevice;
for (const sig of backupSigStatus.sigs) {
if (!sig.device.isVerified()) {
unverifiedDevice = sig.device;
break;
}
}
if (!unverifiedDevice) {
console.log("Unable to find a device to verify.");
return;
}
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().credentials.userId,
device: unverifiedDevice,
onFinished: this.props.onFinished,
});
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const title = <span className="mx_NewRecoveryMethodDialog_title">
{_t("New Recovery Method")}
</span>;
return (
<BaseDialog className="mx_NewRecoveryMethodDialog"
onFinished={this.props.onFinished}
title={title}
hasCancel={false}
>
<div>
<p>{_t(
"A new recovery passphrase and key for Secure " +
"Messages has been detected.",
)}</p>
<p>{_t(
"Setting up Secure Messages on this device " +
"will re-encrypt this device's message history with " +
"the new recovery method.",
)}</p>
<p className="warning">{_t(
"If you didn't set the new recovery method, an " +
"attacker may be trying to access your account. " +
"Change your account password and set a new recovery " +
"method immediately in Settings.",
)}</p>
<DialogButtons
primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick}
cancelButton={_t("Go to Settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>
</BaseDialog>
);
}
}

View File

@ -69,6 +69,7 @@ export default class AutoHideScrollbar extends React.Component {
this.onOverflow = this.onOverflow.bind(this);
this.onUnderflow = this.onUnderflow.bind(this);
this._collectContainerRef = this._collectContainerRef.bind(this);
this._needsOverflowListener = null;
}
onOverflow() {
@ -81,21 +82,35 @@ export default class AutoHideScrollbar extends React.Component {
this.containerRef.classList.add("mx_AutoHideScrollbar_underflow");
}
checkOverflow() {
if (!this._needsOverflowListener) {
return;
}
if (this.containerRef.scrollHeight > this.containerRef.clientHeight) {
this.onOverflow();
} else {
this.onUnderflow();
}
}
componentDidUpdate() {
this.checkOverflow();
}
componentDidMount() {
installBodyClassesIfNeeded();
this._needsOverflowListener =
document.body.classList.contains("mx_scrollbar_nooverlay");
if (this._needsOverflowListener) {
this.containerRef.addEventListener("overflow", this.onOverflow);
this.containerRef.addEventListener("underflow", this.onUnderflow);
}
this.checkOverflow();
}
_collectContainerRef(ref) {
if (ref && !this.containerRef) {
this.containerRef = ref;
const needsOverflowListener =
document.body.classList.contains("mx_scrollbar_nooverlay");
if (needsOverflowListener) {
this.containerRef.addEventListener("overflow", this.onOverflow);
this.containerRef.addEventListener("underflow", this.onUnderflow);
}
if (ref.scrollHeight > ref.clientHeight) {
this.onOverflow();
} else {
this.onUnderflow();
}
}
if (this.props.wrappedRef) {
this.props.wrappedRef(ref);
@ -103,14 +118,13 @@ export default class AutoHideScrollbar extends React.Component {
}
componentWillUnmount() {
if (this.containerRef) {
if (this._needsOverflowListener && this.containerRef) {
this.containerRef.removeEventListener("overflow", this.onOverflow);
this.containerRef.removeEventListener("underflow", this.onUnderflow);
}
}
render() {
installBodyClassesIfNeeded();
return (<div
ref={this._collectContainerRef}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}

View File

@ -473,7 +473,7 @@ export default React.createClass({
GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
let willDoOnboarding = false;
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
GroupStore.on('error', (err, errorGroupId) => {
GroupStore.on('error', (err, errorGroupId, stateKey) => {
if (this._unmounted || groupId !== errorGroupId) return;
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
dis.dispatch({
@ -486,11 +486,13 @@ export default React.createClass({
dis.dispatch({action: 'require_registration'});
willDoOnboarding = true;
}
this.setState({
summary: null,
error: err,
editing: false,
});
if (stateKey === GroupStore.STATE_KEY.Summary) {
this.setState({
summary: null,
error: err,
editing: false,
});
}
});
},
@ -514,7 +516,6 @@ export default React.createClass({
isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
(m) => m.userId === this._matrixClient.credentials.userId,
),
error: null,
});
// XXX: This might not work but this.props.groupIsNew unused anyway
if (this.props.groupIsNew && firstInit) {
@ -1079,6 +1080,7 @@ export default React.createClass({
},
_getJoinableNode: function() {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
return this.state.editing ? <div>
<h3>
{ _t('Who can join this community?') }
@ -1160,7 +1162,7 @@ export default React.createClass({
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
return <Spinner />;
} else if (this.state.summary) {
} else if (this.state.summary && !this.state.error) {
const summary = this.state.summary;
let avatarNode;
@ -1272,15 +1274,6 @@ export default React.createClass({
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
</AccessibleButton>,
);
if (this.props.collapsedRhs) {
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button"
onClick={this._onShowRhsClick} title={_t('Show panel')} key="_maximiseButton"
>
<TintableSvg src="img/maximise.svg" width="10" height="16" />
</AccessibleButton>,
);
}
}
const rightPanel = !this.props.collapsedRhs ? <RightPanel groupId={this.props.groupId} /> : undefined;
@ -1311,7 +1304,7 @@ export default React.createClass({
<div className="mx_GroupView_header_rightCol">
{ rightButtons }
</div>
<GroupHeaderButtons />
<GroupHeaderButtons collapsedRhs={this.props.collapsedRhs} />
</div>
<MainSplit collapsedRhs={this.props.collapsedRhs} panel={rightPanel}>
<GeminiScrollbarWrapper className="mx_GroupView_body">

View File

@ -91,11 +91,15 @@ class HomePage extends React.Component {
this._unmounted = true;
}
onLoginClick() {
onLoginClick(ev) {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({ action: 'start_login' });
}
onRegisterClick() {
onRegisterClick(ev) {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({ action: 'start_registration' });
}

View File

@ -21,41 +21,52 @@ export default class IndicatorScrollbar extends React.Component {
constructor(props) {
super(props);
this._collectScroller = this._collectScroller.bind(this);
this._collectScrollerComponent = this._collectScrollerComponent.bind(this);
this.checkOverflow = this.checkOverflow.bind(this);
this._scrollElement = null;
this._autoHideScrollbar = null;
}
_collectScroller(scroller) {
if (scroller && !this._scroller) {
this._scroller = scroller;
this._scroller.addEventListener("scroll", this.checkOverflow);
if (scroller && !this._scrollElement) {
this._scrollElement = scroller;
this._scrollElement.addEventListener("scroll", this.checkOverflow);
this.checkOverflow();
}
}
_collectScrollerComponent(autoHideScrollbar) {
this._autoHideScrollbar = autoHideScrollbar;
}
checkOverflow() {
const hasTopOverflow = this._scroller.scrollTop > 0;
const hasBottomOverflow = this._scroller.scrollHeight >
(this._scroller.scrollTop + this._scroller.clientHeight);
const hasTopOverflow = this._scrollElement.scrollTop > 0;
const hasBottomOverflow = this._scrollElement.scrollHeight >
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
if (hasTopOverflow) {
this._scroller.classList.add("mx_IndicatorScrollbar_topOverflow");
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
} else {
this._scroller.classList.remove("mx_IndicatorScrollbar_topOverflow");
this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
}
if (hasBottomOverflow) {
this._scroller.classList.add("mx_IndicatorScrollbar_bottomOverflow");
this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
} else {
this._scroller.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
}
if (this._autoHideScrollbar) {
this._autoHideScrollbar.checkOverflow();
}
}
componentWillUnmount() {
if (this._scroller) {
this._scroller.removeEventListener("scroll", this.checkOverflow);
if (this._scrollElement) {
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
}
}
render() {
return (<AutoHideScrollbar wrappedRef={this._collectScroller} {... this.props}>
return (<AutoHideScrollbar ref={this._collectScrollerComponent} wrappedRef={this._collectScroller} {... this.props}>
{ this.props.children }
</AutoHideScrollbar>);
}

View File

@ -151,7 +151,7 @@ const LeftPanel = React.createClass({
}
} while (element && !(
classes.contains("mx_RoomTile") ||
classes.contains("mx_SearchBox_search")));
classes.contains("mx_textinput_search")));
if (element) {
element.focus();

View File

@ -63,7 +63,7 @@ const LoggedInView = React.createClass({
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: PropTypes.func,
collapsedRhs: PropTypes.bool,
teamToken: PropTypes.string,
// Used by the RoomView to handle joining rooms
@ -447,7 +447,7 @@ const LoggedInView = React.createClass({
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}
collapsedRhs={this.props.collapseRhs}
collapsedRhs={this.props.collapsedRhs}
ConferenceHandler={this.props.ConferenceHandler}
/>;
break;
@ -499,7 +499,7 @@ const LoggedInView = React.createClass({
page_element = <GroupView
groupId={this.props.currentGroupId}
isNew={this.props.currentGroupIsNew}
collapsedRhs={this.props.collapseRhs}
collapsedRhs={this.props.collapsedRhs}
/>;
break;
}

View File

@ -41,10 +41,13 @@ export default class MainSplit extends React.Component {
{onResized: this._onResized},
);
resizer.setClassNames(classNames);
const rhsSize = window.localStorage.getItem("mx_rhs_size");
let rhsSize = window.localStorage.getItem("mx_rhs_size");
if (rhsSize !== null) {
resizer.forHandleAt(0).resize(parseInt(rhsSize, 10));
rhsSize = parseInt(rhsSize, 10);
} else {
rhsSize = 350;
}
resizer.forHandleAt(0).resize(rhsSize);
resizer.attach();
this.resizer = resizer;

View File

@ -48,6 +48,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils';
const AutoDiscovery = Matrix.AutoDiscovery;
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
Promise.config({warnings: false});
@ -161,7 +163,7 @@ export default React.createClass({
viewUserId: null,
collapseLhs: false,
collapseRhs: false,
collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true",
leftDisabled: false,
middleDisabled: false,
rightDisabled: false,
@ -181,6 +183,12 @@ export default React.createClass({
register_is_url: null,
register_id_sid: null,
// Parameters used for setting up the login/registration views
defaultServerName: this.props.config.default_server_name,
defaultHsUrl: this.props.config.default_hs_url,
defaultIsUrl: this.props.config.default_is_url,
defaultServerDiscoveryError: null,
// When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs
hideToSRUsers: false,
@ -199,20 +207,24 @@ export default React.createClass({
};
},
getDefaultServerName: function() {
return this.state.defaultServerName;
},
getCurrentHsUrl: function() {
if (this.state.register_hs_url) {
return this.state.register_hs_url;
} else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getHomeserverUrl();
} else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
return window.localStorage.getItem("mx_hs_url");
} else {
return this.getDefaultHsUrl();
}
},
getDefaultHsUrl() {
return this.props.config.default_hs_url || "https://matrix.org";
getDefaultHsUrl(defaultToMatrixDotOrg) {
defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg;
if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org";
return this.state.defaultHsUrl;
},
getFallbackHsUrl: function() {
@ -224,15 +236,13 @@ export default React.createClass({
return this.state.register_is_url;
} else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getIdentityServerUrl();
} else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
return window.localStorage.getItem("mx_is_url");
} else {
return this.getDefaultIsUrl();
}
},
getDefaultIsUrl() {
return this.props.config.default_is_url || "https://vector.im";
return this.state.defaultIsUrl || "https://vector.im";
},
componentWillMount: function() {
@ -282,6 +292,20 @@ export default React.createClass({
console.info(`Team token set to ${this._teamToken}`);
}
// Set up the default URLs (async)
if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) {
this.setState({loadingDefaultHomeserver: true});
this._tryDiscoverDefaultHomeserver(this.getDefaultServerName());
} else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) {
// Ideally we would somehow only communicate this to the server admins, but
// given this is at login time we can't really do much besides hope that people
// will check their settings.
this.setState({
defaultServerName: null, // To un-hide any secrets people might be keeping
defaultServerDiscoveryError: _t("Invalid configuration: Cannot supply a default homeserver URL and a default server name"),
});
}
// Set a default HS with query param `hs_url`
const paramHs = this.props.startingFragmentQueryParams.hs_url;
if (paramHs) {
@ -555,7 +579,7 @@ export default React.createClass({
break;
case 'view_user':
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
if (this.state.collapseRhs) {
if (this.state.collapsedRhs) {
setTimeout(()=>{
dis.dispatch({
action: 'show_right_panel',
@ -659,13 +683,15 @@ export default React.createClass({
});
break;
case 'hide_right_panel':
window.localStorage.setItem("mx_rhs_collapsed", true);
this.setState({
collapseRhs: true,
collapsedRhs: true,
});
break;
case 'show_right_panel':
window.localStorage.setItem("mx_rhs_collapsed", false);
this.setState({
collapseRhs: false,
collapsedRhs: false,
});
break;
case 'panel_disable': {
@ -676,9 +702,11 @@ export default React.createClass({
});
break;
}
case 'set_theme':
this._onSetTheme(payload.value);
break;
// case 'set_theme':
// disable changing the theme for now
// as other themes are not compatible with dharma
// this._onSetTheme(payload.value);
// break;
case 'on_logging_in':
// We are now logging in, so set the state to reflect that
// NB. This does not touch 'ready' since if our dispatches
@ -911,6 +939,10 @@ export default React.createClass({
},
_viewHome: function() {
// The home page requires the "logged in" view, so we'll set that.
this.setStateForNewView({
view: VIEWS.LOGGED_IN,
});
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
},
@ -1167,10 +1199,7 @@ export default React.createClass({
* @param {string} teamToken
*/
_onLoggedIn: async function(teamToken) {
this.setState({
view: VIEWS.LOGGED_IN,
});
this.setStateForNewView({view: VIEWS.LOGGED_IN});
if (teamToken) {
// A team member has logged in, not a guest
this._teamToken = teamToken;
@ -1227,7 +1256,7 @@ export default React.createClass({
view: VIEWS.LOGIN,
ready: false,
collapseLhs: false,
collapseRhs: false,
collapsedRhs: false,
currentRoomId: null,
page_type: PageTypes.RoomDirectory,
});
@ -1418,6 +1447,11 @@ export default React.createClass({
break;
}
});
cli.on("crypto.keyBackupFailed", () => {
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'),
);
});
// 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
@ -1744,6 +1778,36 @@ export default React.createClass({
this.setState(newState);
},
_tryDiscoverDefaultHomeserver: async function(serverName) {
try {
const discovery = await AutoDiscovery.findClientConfig(serverName);
const state = discovery["m.homeserver"].state;
if (state !== AutoDiscovery.SUCCESS) {
console.error("Failed to discover homeserver on startup:", discovery);
this.setState({
defaultServerDiscoveryError: discovery["m.homeserver"].error,
loadingDefaultHomeserver: false,
});
} else {
const hsUrl = discovery["m.homeserver"].base_url;
const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
? discovery["m.identity_server"].base_url
: "https://vector.im";
this.setState({
defaultHsUrl: hsUrl,
defaultIsUrl: isUrl,
loadingDefaultHomeserver: false,
});
}
} catch (e) {
console.error(e);
this.setState({
defaultServerDiscoveryError: _t("Unknown error discovering homeserver"),
loadingDefaultHomeserver: false,
});
}
},
_makeRegistrationUrl: function(params) {
if (this.props.startingFragmentQueryParams.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer;
@ -1758,7 +1822,7 @@ export default React.createClass({
render: function() {
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) {
if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN || this.state.loadingDefaultHomeserver) {
const Spinner = sdk.getComponent('elements.Spinner');
return (
<div className="mx_MatrixChat_splash">
@ -1832,6 +1896,8 @@ export default React.createClass({
idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email}
referrer={this.props.startingFragmentQueryParams.referrer}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
@ -1854,6 +1920,8 @@ export default React.createClass({
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
return (
<ForgotPassword
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
@ -1870,6 +1938,8 @@ export default React.createClass({
<Login
onLoggedIn={Lifecycle.setLoggedIn}
onRegisterClick={this.onRegisterClick}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}

View File

@ -110,8 +110,9 @@ const RoomSubList = React.createClass({
if (this.isCollapsableOnClick()) {
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden;
this.setState({hidden: isHidden});
this.props.onHeaderClick(isHidden);
this.setState({hidden: isHidden}, () => {
this.props.onHeaderClick(isHidden);
});
} else {
// The header is stuck, so the click is to be interpreted as a scroll to the header
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
@ -268,17 +269,10 @@ const RoomSubList = React.createClass({
let incomingCall;
if (this.props.incomingCall) {
const self = this;
// Check if the incoming call is for this section
const incomingCallRoom = this.props.list.filter(function(room) {
return self.props.incomingCall.roomId === room.roomId;
});
if (incomingCallRoom.length === 1) {
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
incomingCall =
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
}
// We can assume that if we have an incoming call then it is for this list
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
incomingCall =
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
}
let addRoomButton;
@ -313,6 +307,12 @@ const RoomSubList = React.createClass({
);
},
checkOverflow: function() {
if (this.refs.scroller) {
this.refs.scroller.checkOverflow();
}
},
render: function() {
const len = this.props.list.length + this.props.extraTiles.length;
if (len) {
@ -330,7 +330,7 @@ const RoomSubList = React.createClass({
tiles.push(...this.props.extraTiles);
return <div className={subListClasses}>
{this._getHeaderJsx()}
<IndicatorScrollbar className="mx_RoomSubList_scroll">
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
{ tiles }
</IndicatorScrollbar>
</div>;

View File

@ -27,6 +27,7 @@ const React = require("react");
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
import Promise from 'bluebird';
import filesize from 'filesize';
const classNames = require("classnames");
import { _t } from '../../languageHandler';
@ -103,6 +104,10 @@ module.exports = React.createClass({
roomLoading: true,
peekLoading: false,
shouldPeek: true,
// Media limits for uploading.
mediaConfig: undefined,
// used to trigger a rerender in TimelinePanel once the members are loaded,
// so RR are rendered again (now with the members available), ...
membersLoaded: !llMembers,
@ -158,7 +163,8 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
this._fetchMediaConfig();
// Start listening for RoomViewStore updates
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true);
@ -166,6 +172,27 @@ module.exports = React.createClass({
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
},
_fetchMediaConfig: function(invalidateCache: boolean = false) {
/// NOTE: Using global here so we don't make repeated requests for the
/// config every time we swap room.
if(global.mediaConfig !== undefined && !invalidateCache) {
this.setState({mediaConfig: global.mediaConfig});
return;
}
console.log("[Media Config] Fetching");
MatrixClientPeg.get().getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
}).then((config) => {
global.mediaConfig = config;
this.setState({mediaConfig: config});
});
},
_onRoomViewStoreUpdate: function(initial) {
if (this.unmounted) {
return;
@ -424,6 +451,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
}
window.removeEventListener('beforeunload', this.onPageUnload);
@ -500,6 +528,10 @@ module.exports = React.createClass({
break;
case 'notifier_enabled':
case 'upload_failed':
// 413: File was too big or upset the server in some way.
if(payload.error.http_status === 413) {
this._fetchMediaConfig(true);
}
case 'upload_started':
case 'upload_finished':
this.forceUpdate();
@ -578,6 +610,25 @@ module.exports = React.createClass({
}
},
async onRoomRecoveryReminderFinished(backupCreated) {
// If the user cancelled the key backup dialog, it suggests they don't
// want to be reminded anymore.
if (!backupCreated) {
await SettingsStore.setValue(
"showRoomRecoveryReminder",
null,
SettingLevel.ACCOUNT,
false,
);
}
},
onKeyBackupStatus() {
// Key backup status changes affect whether the in-room recovery
// reminder is displayed.
this.forceUpdate();
},
canResetTimeline: function() {
if (!this.refs.messagePanel) {
return true;
@ -932,6 +983,15 @@ module.exports = React.createClass({
this.setState({ draggingFile: false });
},
isFileUploadAllowed(file) {
if (this.state.mediaConfig !== undefined &&
this.state.mediaConfig["m.upload.size"] !== undefined &&
file.size > this.state.mediaConfig["m.upload.size"]) {
return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.state.mediaConfig["m.upload.size"])});
}
return true;
},
uploadFile: async function(file) {
this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'});
@ -1483,6 +1543,7 @@ module.exports = React.createClass({
const Loader = sdk.getComponent("elements.Spinner");
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
if (!this.state.room) {
if (this.state.roomLoading || this.state.peekLoading) {
@ -1622,6 +1683,13 @@ module.exports = React.createClass({
this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId)
);
const showRoomRecoveryReminder = (
SettingsStore.isFeatureEnabled("feature_keybackup") &&
SettingsStore.getValue("showRoomRecoveryReminder") &&
MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) &&
!MatrixClientPeg.get().getKeyBackupEnabled()
);
let aux = null;
let hideCancel = false;
if (this.state.editingRoomSettings) {
@ -1636,6 +1704,9 @@ module.exports = React.createClass({
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} />;
hideCancel = true;
} else if (showRoomRecoveryReminder) {
aux = <RoomRecoveryReminder onFinished={this.onRoomRecoveryReminderFinished} />;
hideCancel = true;
} else if (this.state.showingPinned) {
hideCancel = true; // has own cancel
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
@ -1696,6 +1767,7 @@ module.exports = React.createClass({
callState={this.state.callState}
disabled={this.props.disabled}
showApps={this.state.showApps}
uploadAllowed={this.isFileUploadAllowed}
/>;
}

View File

@ -56,7 +56,6 @@ module.exports = React.createClass({
case 'focus_room_filter':
if (this.refs.search) {
this.refs.search.focus();
this.refs.search.select();
}
break;
}
@ -83,6 +82,10 @@ module.exports = React.createClass({
}
},
_onFocus: function(ev) {
ev.target.select();
},
_clearSearch: function(source) {
this.refs.search.value = "";
this.onChange();
@ -108,6 +111,7 @@ module.exports = React.createClass({
ref="search"
className="mx_textinput_icon mx_textinput_search"
value={ this.state.searchTerm }
onFocus={ this._onFocus }
onChange={ this.onChange }
onKeyDown={ this._onKeyDown }
placeholder={ _t('Filter room names') }

View File

@ -23,6 +23,7 @@ import GroupActions from '../../actions/GroupActions';
import sdk from '../../index';
import dis from '../../dispatcher';
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import { Droppable } from 'react-beautiful-dnd';
@ -47,6 +48,8 @@ const TagPanel = React.createClass({
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.on("sync", this._onClientSync);
this._dispatcherRef = dis.register(this._onAction);
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
if (this.unmounted) {
return;
@ -67,6 +70,9 @@ const TagPanel = React.createClass({
if (this._filterStoreToken) {
this._filterStoreToken.remove();
}
if (this._dispatcherRef) {
dis.unregister(this._dispatcherRef);
}
},
_onGroupMyMembership() {
@ -100,13 +106,21 @@ const TagPanel = React.createClass({
dis.dispatch({action: 'deselect_tags'});
},
_onAction(payload) {
if (payload.action === "show_redesign_feedback_dialog") {
const RedesignFeedbackDialog =
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createDialog(RedesignFeedbackDialog);
}
},
render() {
const GroupsButton = sdk.getComponent('elements.GroupsButton');
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const ActionButton = sdk.getComponent("elements.ActionButton");
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
@ -162,7 +176,10 @@ const TagPanel = React.createClass({
</GeminiScrollbarWrapper>
<div className="mx_TagPanel_divider" />
<div className="mx_TagPanel_groupsButton">
<GroupsButton tooltip={true} />
<GroupsButton />
<ActionButton
className="mx_TagPanel_report" action="show_redesign_feedback_dialog"
label={_t("Report bugs & give feedback")} tooltip={true} />
</div>
</div>;
},

View File

@ -64,6 +64,7 @@ const SIMPLE_SETTINGS = [
{ id: "urlPreviewsEnabled" },
{ id: "autoplayGifsAndVideos" },
{ id: "alwaysShowEncryptionIcons" },
{ id: "showRoomRecoveryReminder" },
{ id: "hideReadReceipts" },
{ id: "dontSendTypingNotifications" },
{ id: "alwaysShowTimestamps" },
@ -188,9 +189,11 @@ module.exports = React.createClass({
phase: "UserSettings.LOADING", // LOADING, DISPLAY
email_add_pending: false,
vectorVersion: undefined,
canSelfUpdate: null,
rejectingInvites: false,
mediaDevices: null,
ignoredUsers: [],
autoLaunchEnabled: null,
};
},
@ -209,6 +212,13 @@ module.exports = React.createClass({
}, (e) => {
console.log("Failed to fetch app version", e);
});
PlatformPeg.get().canSelfUpdate().then((canUpdate) => {
if (this._unmounted) return;
this.setState({
canSelfUpdate: canUpdate,
});
});
}
this._refreshMediaDevices();
@ -227,11 +237,12 @@ module.exports = React.createClass({
});
this._refreshFromServer();
if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron');
ipcRenderer.on('settings', this._electronSettings);
ipcRenderer.send('settings_get');
if (PlatformPeg.get().supportsAutoLaunch()) {
PlatformPeg.get().getAutoLaunchEnabled().then(enabled => {
this.setState({
autoLaunchEnabled: enabled,
});
});
}
this.setState({
@ -262,11 +273,6 @@ module.exports = React.createClass({
if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
}
if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron');
ipcRenderer.removeListener('settings', this._electronSettings);
}
},
// `UserSettings` assumes that the client peg will not be null, so give it some
@ -285,10 +291,6 @@ module.exports = React.createClass({
});
},
_electronSettings: function(ev, settings) {
this.setState({ electron_settings: settings });
},
_refreshMediaDevices: function(stream) {
if (stream) {
// kill stream so that we don't leave it lingering around with webcam enabled etc
@ -943,7 +945,7 @@ module.exports = React.createClass({
_renderCheckUpdate: function() {
const platform = PlatformPeg.get();
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
if (this.state.canSelfUpdate) {
return <div>
<h3>{ _t('Updates') }</h3>
<div className="mx_UserSettings_section">
@ -988,8 +990,7 @@ module.exports = React.createClass({
},
_renderElectronSettings: function() {
const settings = this.state.electron_settings;
if (!settings) return;
if (!PlatformPeg.get().supportsAutoLaunch()) return;
// TODO: This should probably be a granular setting, but it only applies to electron
// and ends up being get/set outside of matrix anyways (local system setting).
@ -999,7 +1000,7 @@ module.exports = React.createClass({
<div className="mx_UserSettings_toggle">
<input type="checkbox"
name="auto-launch"
defaultChecked={settings['auto-launch']}
defaultChecked={this.state.autoLaunchEnabled}
onChange={this._onAutoLaunchChanged}
/>
<label htmlFor="auto-launch">{ _t('Start automatically after system login') }</label>
@ -1009,8 +1010,11 @@ module.exports = React.createClass({
},
_onAutoLaunchChanged: function(e) {
const {ipcRenderer} = require('electron');
ipcRenderer.send('settings_set', 'auto-launch', e.target.checked);
PlatformPeg.get().setAutoLaunchEnabled(e.target.checked).then(() => {
this.setState({
autoLaunchEnabled: e.target.checked,
});
});
},
_mapWebRtcDevicesToSpans: function(devices) {
@ -1369,7 +1373,7 @@ module.exports = React.createClass({
{ this._renderBulkOptions() }
{ this._renderBugReport() }
{ PlatformPeg.get().isElectron() && this._renderElectronSettings() }
{ this._renderElectronSettings() }
{ this._renderAnalyticsControl() }

View File

@ -36,6 +36,14 @@ module.exports = React.createClass({
onLoginClick: PropTypes.func,
onRegisterClick: PropTypes.func,
onComplete: PropTypes.func.isRequired,
// The default server name to use when the user hasn't specified
// one. This is used when displaying the defaultHsUrl in the UI.
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
},
getInitialState: function() {
@ -45,6 +53,7 @@ module.exports = React.createClass({
progress: null,
password: null,
password2: null,
errorText: null,
};
},
@ -81,6 +90,13 @@ module.exports = React.createClass({
onSubmitForm: function(ev) {
ev.preventDefault();
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
if (!this.state.email) {
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} else if (!this.state.password || !this.state.password2) {
@ -146,6 +162,18 @@ module.exports = React.createClass({
this.setState(newState);
},
onLoginClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onLoginClick();
},
onRegisterClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
showErrorDialog: function(body, title) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
@ -200,6 +228,12 @@ module.exports = React.createClass({
);
}
let errorText = null;
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
if (err) {
errorText = <div className="mx_Login_error">{ err }</div>;
}
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
resetPasswordJsx = (
@ -230,10 +264,11 @@ module.exports = React.createClass({
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
</form>
{ serverConfigSection }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ errorText }
<a className="mx_Login_create" onClick={this.onLoginClick} href="#">
{ _t('Return to login screen') }
</a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
<a className="mx_Login_create" onClick={this.onRegisterClick} href="#">
{ _t('Create an account') }
</a>
<LanguageSelector />

View File

@ -26,10 +26,17 @@ import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import { AutoDiscovery } from "matrix-js-sdk";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
// These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n.
_td("Invalid homeserver discovery response");
_td("Invalid identity server discovery response");
_td("General failure");
/**
* A wire component which glues together login UI components and Login logic
*/
@ -50,6 +57,14 @@ module.exports = React.createClass({
// different home server without confusing users.
fallbackHsUrl: PropTypes.string,
// The default server name to use when the user hasn't specified
// one. This is used when displaying the defaultHsUrl in the UI.
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
defaultDeviceDisplayName: PropTypes.string,
// login shouldn't know or care how registration is done.
@ -74,6 +89,12 @@ module.exports = React.createClass({
phoneCountry: null,
phoneNumber: "",
currentFlow: "m.login.password",
// .well-known discovery
discoveredHsUrl: "",
discoveredIsUrl: "",
discoveryError: "",
findingHomeserver: false,
};
},
@ -105,6 +126,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 || this.props.defaultServerDiscoveryError) return;
this.setState({
busy: true,
errorText: null,
@ -189,7 +214,10 @@ module.exports = React.createClass({
}).done();
},
_onLoginAsGuestClick: function() {
_onLoginAsGuestClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
const self = this;
self.setState({
busy: true,
@ -221,6 +249,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 });
},
@ -256,6 +300,65 @@ module.exports = React.createClass({
});
},
onRegisterClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
_tryWellKnownDiscovery: async function(serverName) {
if (!serverName.trim()) {
// Nothing to discover
this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: "", findingHomeserver: false});
return;
}
this.setState({findingHomeserver: true});
try {
const discovery = await AutoDiscovery.findClientConfig(serverName);
const state = discovery["m.homeserver"].state;
if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
this.setState({
discoveredHsUrl: "",
discoveredIsUrl: "",
discoveryError: discovery["m.homeserver"].error,
findingHomeserver: false,
});
} else if (state === AutoDiscovery.PROMPT) {
this.setState({
discoveredHsUrl: "",
discoveredIsUrl: "",
discoveryError: "",
findingHomeserver: false,
});
} else if (state === AutoDiscovery.SUCCESS) {
this.setState({
discoveredHsUrl: discovery["m.homeserver"].base_url,
discoveredIsUrl:
discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
? discovery["m.identity_server"].base_url
: "",
discoveryError: "",
findingHomeserver: false,
});
} else {
console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
this.setState({
discoveredHsUrl: "",
discoveredIsUrl: "",
discoveryError: _t("Unknown failure discovering homeserver"),
findingHomeserver: false,
});
}
} catch (e) {
console.error(e);
this.setState({
findingHomeserver: false,
discoveryError: _t("Unknown error discovering homeserver"),
});
}
},
_initLoginLogic: function(hsUrl, isUrl) {
const self = this;
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
@ -393,11 +496,14 @@ 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}
loginIncorrect={this.state.loginIncorrect}
hsUrl={this.state.enteredHomeserverUrl}
hsName={this.props.defaultServerName}
disableSubmit={this.state.findingHomeserver}
/>
);
},
@ -416,6 +522,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.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
let loginAsGuestJsx;
if (this.props.enableGuest) {
loginAsGuestJsx =
@ -430,8 +538,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}
@ -443,16 +551,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>
);
}
@ -468,7 +576,7 @@ module.exports = React.createClass({
{ errorTextSection }
{ this.componentForStep(this.state.currentFlow) }
{ serverConfig }
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
<a className="mx_Login_create" onClick={this.onRegisterClick} href="#">
{ _t('Create an account') }
</a>
{ loginAsGuestJsx }

View File

@ -57,6 +57,14 @@ module.exports = React.createClass({
}),
teamSelected: PropTypes.object,
// The default server name to use when the user hasn't specified
// one. This is used when displaying the defaultHsUrl in the UI.
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
defaultDeviceDisplayName: PropTypes.string,
// registration shouldn't know or care how login is done.
@ -170,6 +178,12 @@ module.exports = React.createClass({
},
onFormSubmit: function(formVals) {
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
this.setState({
errorText: "",
busy: true,
@ -328,7 +342,7 @@ module.exports = React.createClass({
errMsg = _t('A phone number is required to register on this homeserver.');
break;
case "RegistrationForm.ERR_USERNAME_INVALID":
errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.');
errMsg = _t("Only use lower case letters, numbers and '=_-./'");
break;
case "RegistrationForm.ERR_USERNAME_BLANK":
errMsg = _t('You need to enter a user name.');
@ -349,6 +363,12 @@ module.exports = React.createClass({
}
},
onLoginClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onLoginClick();
},
_makeRegisterRequest: function(auth) {
// Only send the bind params if we're sending username / pw params
// (Since we need to send no params at all to use the ones saved in the
@ -441,19 +461,20 @@ module.exports = React.createClass({
let header;
let errorText;
// FIXME: remove hardcoded Status team tweaks at some point
if (theme === 'status' && this.state.errorText) {
header = <div className="mx_Login_error">{ this.state.errorText }</div>;
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
if (theme === 'status' && err) {
header = <div className="mx_Login_error">{ err }</div>;
} else {
header = <h2>{ _t('Create an account') }</h2>;
if (this.state.errorText) {
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;
if (err) {
errorText = <div className="mx_Login_error">{ err }</div>;
}
}
let signIn;
if (!this.state.doingUIAuth) {
signIn = (
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
<a className="mx_Login_create" onClick={this.onLoginClick} href="#">
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
</a>
);

View File

@ -0,0 +1,120 @@
/*
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 PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import MemberAvatar from '../avatars/MemberAvatar';
import classNames from 'classnames';
import * as ContextualMenu from "../../structures/ContextualMenu";
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
export default class MemberStatusMessageAvatar extends React.Component {
static propTypes = {
member: PropTypes.object.isRequired,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
};
static defaultProps = {
width: 40,
height: 40,
resizeMethod: 'crop',
};
constructor(props, context) {
super(props, context);
}
componentWillMount() {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
}
}
componentDidMount() {
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
if (this.props.member.user) {
this.setState({message: this.props.member.user._unstable_statusMessage});
} else {
this.setState({message: ""});
}
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
}
}
_onRoomStateEvents = (ev, state) => {
if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return;
if (ev.getType() !== "im.vector.user_status") return;
// TODO: We should be relying on `this.props.member.user._unstable_statusMessage`
// We don't currently because the js-sdk doesn't emit a specific event for this
// change, and we don't want to race it. This should be improved when we rip out
// the im.vector.user_status stuff and replace it with a complete solution.
this.setState({message: ev.getContent()["status"]});
};
_onClick = (e) => {
e.stopPropagation();
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
const chevronOffset = 12;
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
ContextualMenu.createMenu(StatusMessageContextMenu, {
chevronOffset: chevronOffset,
chevronFace: 'bottom',
left: x,
top: y,
menuWidth: 190,
user: this.props.member.user,
});
};
render() {
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return <MemberAvatar member={this.props.member}
width={this.props.width}
height={this.props.height}
resizeMethod={this.props.resizeMethod} />;
}
const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false;
const classes = classNames({
"mx_MemberStatusMessageAvatar": true,
"mx_MemberStatusMessageAvatar_hasStatus": hasStatus,
});
return <AccessibleButton onClick={this._onClick} className={classes} element="div">
<MemberAvatar member={this.props.member}
width={this.props.width}
height={this.props.height}
resizeMethod={this.props.resizeMethod} />
</AccessibleButton>;
}
}

View File

@ -0,0 +1,86 @@
/*
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import classNames from 'classnames';
export default class StatusMessageContextMenu extends React.Component {
static propTypes = {
// js-sdk User object. Not required because it might not exist.
user: PropTypes.object,
};
constructor(props, context) {
super(props, context);
this.state = {
message: props.user ? props.user._unstable_statusMessage : "",
};
}
_onClearClick = async(e) => {
await MatrixClientPeg.get()._unstable_setStatusMessage("");
this.setState({message: ""});
};
_onSubmit = (e) => {
e.preventDefault();
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
};
_onStatusChange = (e) => {
this.setState({message: e.target.value});
};
render() {
const formSubmitClasses = classNames({
"mx_StatusMessageContextMenu_submit": true,
"mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded
});
const form = <form className="mx_StatusMessageContextMenu_form" onSubmit={this._onSubmit} autoComplete="off">
<input type="text" key="message" placeholder={_t("Set a new status...")} autoFocus={true}
className="mx_StatusMessageContextMenu_message"
value={this.state.message} onChange={this._onStatusChange} maxLength="60" />
<AccessibleButton onClick={this._onSubmit} element="div" className={formSubmitClasses}>
<img src="img/icons-checkmark.svg" width="22" height="22" />
</AccessibleButton>
</form>;
const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg";
const clearButton = <AccessibleButton onClick={this._onClearClick} disabled={!this.state.message}
className="mx_StatusMessageContextMenu_clear">
<img src={clearIcon} alt={_t('Clear status')} width="12" height="12"
className="mx_filterFlipColor mx_StatusMessageContextMenu_clearIcon" />
<span>{_t("Clear status")}</span>
</AccessibleButton>;
const menuClasses = classNames({
"mx_StatusMessageContextMenu": true,
"mx_StatusMessageContextMenu_hasStatus": this.state.message,
});
return <div className={menuClasses}>
{ form }
<hr />
{ clearButton }
</div>;
}
}

View File

@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from "../../../email";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -419,6 +420,10 @@ module.exports = React.createClass({
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (this.props.validAddressTypes.includes(addrType)) {
if (addrType === 'email' && !Email.looksValid(query)) {
this.setState({searchError: _t("That doesn't look like a valid email address")});
return;
}
suggestedList.unshift({
addressType: addrType,
address: query,

View File

@ -57,8 +57,7 @@ export default React.createClass({
className: PropTypes.string,
// Title for the dialog.
// (could probably actually be something more complicated than a string if desired)
title: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
// children should be the content of the dialog
children: PropTypes.node,

View File

@ -36,8 +36,12 @@ export default class ChangelogDialog extends React.Component {
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;
const url = `https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`;
request(url, (err, response, body) => {
if (response.statusCode < 200 || response.statusCode >= 300) {
this.setState({ [REPOS[i]]: response.statusText });
return;
}
this.setState({[REPOS[i]]: JSON.parse(body).commits});
});
}
@ -58,13 +62,20 @@ export default class ChangelogDialog extends React.Component {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
const logs = REPOS.map(repo => {
if (this.state[repo] == null) return <Spinner key={repo} />;
let content;
if (this.state[repo] == null) {
content = <Spinner key={repo} />;
} else if (typeof this.state[repo] === "string") {
content = _t("Unable to load commit detail: %(msg)s", {
msg: this.state[repo],
});
} else {
content = this.state[repo].map(this._elementsForCommit);
}
return (
<div key={repo}>
<h2>{repo}</h2>
<ul>
{this.state[repo].map(this._elementsForCommit)}
</ul>
<ul>{content}</ul>
</div>
);
});

View File

@ -35,19 +35,10 @@ export default class DeactivateAccountDialog extends React.Component {
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
const deactivationPreferences =
MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences');
const shouldErase = (
deactivationPreferences &&
deactivationPreferences.getContent() &&
deactivationPreferences.getContent().shouldErase
) || false;
this.state = {
confirmButtonEnabled: false,
busy: false,
shouldErase,
shouldErase: false,
errStr: null,
};
}
@ -67,36 +58,6 @@ export default class DeactivateAccountDialog extends React.Component {
async _onOk() {
this.setState({busy: true});
// Before we deactivate the account insert an event into
// the user's account data indicating that they wish to be
// erased from the homeserver.
//
// We do this because the API for erasing after deactivation
// might not be supported by the connected homeserver. Leaving
// an indication in account data is only best-effort, and
// in the worse case, the HS maintainer would have to run a
// script to erase deactivated accounts that have shouldErase
// set to true in im.riot.account_deactivation_preferences.
//
// Note: The preferences are scoped to Riot, hence the
// "im.riot..." event type.
//
// Note: This may have already been set on previous attempts
// where, for example, the user entered the wrong password.
// This is fine because the UI always indicates the preference
// prior to us calling `deactivateAccount`.
try {
await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', {
shouldErase: this.state.shouldErase,
});
} catch (err) {
this.setState({
busy: false,
errStr: _t('Failed to indicate account erasure'),
});
return;
}
try {
// This assumes that the HS requires password UI auth
// for this endpoint. In reality it could be any UI auth.

View File

@ -0,0 +1,51 @@
/*
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 QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
export default (props) => {
const existingIssuesUrl = "https://github.com/vector-im/riot-web/issues" +
"?q=is%3Aopen+is%3Aissue+label%3Aredesign+sort%3Areactions-%2B1-desc";
const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new" +
"?assignees=&labels=redesign&template=redesign_issue.md&title=";
const description1 =
_t("Thanks for testing the Riot Redesign. " +
"If you run into any bugs or visual issues, " +
"please let us know on GitHub.");
const description2 = _t("To help avoid duplicate issues, " +
"please <existingIssuesLink>view existing issues</existingIssuesLink> " +
"first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> " +
"if you can't find it.", {},
{
existingIssuesLink: (sub) => {
return <a target="_blank" rel="noreferrer noopener" href={existingIssuesUrl}>{ sub }</a>;
},
newIssueLink: (sub) => {
return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
},
});
return (<QuestionDialog
hasCancelButton={false}
title={_t("Report bugs & give feedback")}
description={<div><p>{description1}</p><p>{description2}</p></div>}
button={_t("Go back")}
onFinished={props.onFinished}
/>);
};

View File

@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import { KeyCode } from '../../../Keyboard';
import { _t } from '../../../languageHandler';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
// The amount of time to wait for further changes to the input username before
// sending a request to the server
@ -110,12 +111,11 @@ export default React.createClass({
},
_doUsernameCheck: function() {
// XXX: SPEC-1
// Check if username is valid
// Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190
if (encodeURIComponent(this.state.username) !== this.state.username) {
// We do a quick check ahead of the username availability API to ensure the
// user ID roughly looks okay from a Matrix perspective.
if (!SAFE_LOCALPART_REGEX.test(this.state.username)) {
this.setState({
usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'),
usernameError: _t("Only use lower case letters, numbers and '=_-./'"),
});
return Promise.reject();
}
@ -210,7 +210,6 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent('elements.Spinner');
let auth;
if (this.state.doingUIAuth) {
@ -230,9 +229,8 @@ export default React.createClass({
});
let usernameIndicator = null;
let usernameBusyIndicator = null;
if (this.state.usernameBusy) {
usernameBusyIndicator = <Spinner w="24" h="24" />;
usernameIndicator = <div>{_t("Checking...")}</div>;
} else {
const usernameAvailable = this.state.username &&
this.state.usernameCheckSupport && !this.state.usernameError;
@ -270,7 +268,6 @@ export default React.createClass({
size="30"
className={inputClasses}
/>
{ usernameBusyIndicator }
</div>
{ usernameIndicator }
<p>

View File

@ -31,6 +31,7 @@ export default React.createClass({
mouseOverAction: PropTypes.string,
label: PropTypes.string.isRequired,
iconPath: PropTypes.string,
className: PropTypes.string,
},
getDefaultProps: function() {
@ -76,8 +77,13 @@ export default React.createClass({
(<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />) :
undefined;
const classNames = ["mx_RoleButton"];
if (this.props.className) {
classNames.push(this.props.className);
}
return (
<AccessibleButton className="mx_RoleButton"
<AccessibleButton className={classNames.join(" ")}
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}

View File

@ -22,7 +22,6 @@ import qs from 'querystring';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import TintableSvgButton from './TintableSvgButton';
@ -49,7 +48,6 @@ export default class AppTile extends React.Component {
this.state = this._getNewState(props);
this._onAction = this._onAction.bind(this);
this._onMessage = this._onMessage.bind(this);
this._onLoaded = this._onLoaded.bind(this);
this._onEditClick = this._onEditClick.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
@ -143,10 +141,6 @@ export default class AppTile extends React.Component {
}
componentDidMount() {
// Legacy Jitsi widget messaging -- TODO replace this with standard widget
// postMessaging API
window.addEventListener('message', this._onMessage, false);
// Widget action listeners
this.dispatcherRef = dis.register(this._onAction);
}
@ -155,9 +149,6 @@ export default class AppTile extends React.Component {
// Widget action listeners
dis.unregister(this.dispatcherRef);
// Jitsi listener
window.removeEventListener('message', this._onMessage);
// if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
ActiveWidgetStore.destroyPersistentWidget();
@ -233,32 +224,6 @@ export default class AppTile extends React.Component {
}
}
// Legacy Jitsi widget messaging
// TODO -- This should be replaced with the new widget postMessaging API
_onMessage(event) {
if (this.props.type !== 'jitsi') {
return;
}
if (!event.origin) {
event.origin = event.originalEvent.origin;
}
const widgetUrlObj = url.parse(this.state.widgetUrl);
const eventOrigin = url.parse(event.origin);
if (
eventOrigin.protocol !== widgetUrlObj.protocol ||
eventOrigin.host !== widgetUrlObj.host
) {
return;
}
if (event.data.widgetAction === 'jitsi_iframe_loaded') {
const iframe = this.refs.appFrame.contentWindow
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
PlatformPeg.get().setupScreenSharingForIframe(iframe);
}
}
_canUserModify() {
// User widgets should always be modifiable by their creator
if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) {
@ -544,7 +509,7 @@ export default class AppTile extends React.Component {
// Additional iframe feature pemissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
const iframeFeatures = "microphone; camera; encrypted-media;";
const iframeFeatures = "microphone; camera; encrypted-media; autoplay;";
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');

View File

@ -22,17 +22,16 @@ import { _t } from '../../../languageHandler';
const GroupsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_my_groups"
<ActionButton className="mx_GroupsButton" action="view_my_groups"
label={_t("Communities")}
size={props.size}
tooltip={props.tooltip}
tooltip={true}
/>
);
};
GroupsButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default GroupsButton;

View File

@ -91,7 +91,7 @@ export default class ManageIntegsButton extends React.Component {
integrationsButton = (
<AccessibleButton className={integrationsButtonClasses} onClick={this.onManageIntegrations} title={_t('Manage Integrations')}>
<TintableSvg src="img/icons-apps.svg" width="35" height="35" />
<TintableSvg src="img/feather-icons/grid.svg" width="20" height="20" />
{ integrationsWarningTriangle }
{ integrationsErrorPopup }
</AccessibleButton>

View File

@ -14,13 +14,14 @@ const ResizeHandle = (props) => {
classNames.push('mx_ResizeHandle_reverse');
}
return (
<div className={classNames.join(' ')} data-id={props.id} />
<div className={classNames.join(' ')} data-id={props.id}><div /></div>
);
};
ResizeHandle.propTypes = {
vertical: PropTypes.bool,
reverse: PropTypes.bool,
id: PropTypes.string,
};
export default ResizeHandle;

View File

@ -37,7 +37,9 @@ export default React.createClass({
getInitialState: function() {
return {
members: null,
membersError: null,
invitedMembers: null,
invitedMembersError: null,
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
};
},
@ -55,6 +57,19 @@ export default React.createClass({
GroupStore.registerListener(groupId, () => {
this._fetchMembers();
});
GroupStore.on('error', (err, errorGroupId, stateKey) => {
if (this._unmounted || groupId !== errorGroupId) return;
if (stateKey === GroupStore.STATE_KEY.GroupMembers) {
this.setState({
membersError: err,
});
}
if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers) {
this.setState({
invitedMembersError: err,
});
}
});
},
_fetchMembers: function() {
@ -88,7 +103,11 @@ export default React.createClass({
this.setState({ searchQuery: ev.target.value });
},
makeGroupMemberTiles: function(query, memberList) {
makeGroupMemberTiles: function(query, memberList, memberListError) {
if (memberListError) {
return <div className="warning">{ _t("Failed to load group members") }</div>;
}
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
const TruncatedList = sdk.getComponent("elements.TruncatedList");
query = (query || "").toLowerCase();
@ -166,13 +185,25 @@ export default React.createClass({
);
const joined = this.state.members ? <div className="mx_MemberList_joined">
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.members) }
{
this.makeGroupMemberTiles(
this.state.searchQuery,
this.state.members,
this.state.membersError,
)
}
</div> : <div />;
const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
<div className="mx_MemberList_invited">
<h2>{ _t("Invited") }</h2>
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.invitedMembers) }
<h2>{_t("Invited")}</h2>
{
this.makeGroupMemberTiles(
this.state.searchQuery,
this.state.invitedMembers,
this.state.invitedMembersError,
)
}
</div> : <div />;
let inviteButton;

View File

@ -30,6 +30,7 @@ class PasswordLogin extends React.Component {
static defaultProps = {
onError: function() {},
onUsernameChanged: function() {},
onUsernameBlur: function() {},
onPasswordChanged: function() {},
onPhoneCountryChanged: function() {},
onPhoneNumberChanged: function() {},
@ -39,6 +40,8 @@ class PasswordLogin extends React.Component {
initialPassword: "",
loginIncorrect: false,
hsDomain: "",
hsName: null,
disableSubmit: false,
}
constructor(props) {
@ -53,6 +56,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 +128,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 +175,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 +191,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?:\/\//, ''),
@ -242,13 +252,15 @@ class PasswordLogin extends React.Component {
);
}
let matrixIdText = '';
if (this.props.hsUrl) {
let matrixIdText = _t('Matrix ID');
if (this.props.hsName) {
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName});
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname});
} catch (e) {
// pass
// ignore
}
}
@ -280,6 +292,8 @@ class PasswordLogin extends React.Component {
);
}
const disableSubmit = this.props.disableSubmit || matrixIdText === '';
return (
<div>
<form onSubmit={this.onSubmitForm}>
@ -293,7 +307,7 @@ class PasswordLogin extends React.Component {
/>
<br />
{ forgotPasswordJsx }
<input className="mx_Login_submit" type="submit" value={_t('Sign in')} disabled={matrixIdText === ''} />
<input className="mx_Login_submit" type="submit" value={_t('Sign in')} disabled={disableSubmit} />
</form>
</div>
);
@ -317,6 +331,8 @@ PasswordLogin.propTypes = {
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
hsName: PropTypes.string,
disableSubmit: PropTypes.bool,
};
module.exports = PasswordLogin;

View File

@ -25,7 +25,7 @@ import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country';
@ -194,9 +194,8 @@ module.exports = React.createClass({
} else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break;
case FIELD_USERNAME:
// XXX: SPEC-1
var username = this.refs.username.value.trim();
if (encodeURIComponent(username) != username) {
const username = this.refs.username.value.trim();
if (!SAFE_LOCALPART_REGEX.test(username)) {
this.markFieldValid(
field_id,
false,

View File

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

View File

@ -85,8 +85,8 @@ export default React.createClass({
_getDisplayedGroups(userGroups, relatedGroups) {
let displayedGroups = userGroups || [];
if (relatedGroups && relatedGroups.length > 0) {
displayedGroups = displayedGroups.filter((groupId) => {
return relatedGroups.includes(groupId);
displayedGroups = relatedGroups.filter((groupId) => {
return displayedGroups.includes(groupId);
});
} else {
displayedGroups = [];

View File

@ -55,23 +55,23 @@ export default class GroupHeaderButtons extends HeaderButtons {
}
renderButtons() {
const isPhaseGroup = [
const groupPhases = [
RightPanel.Phase.GroupMemberInfo,
RightPanel.Phase.GroupMemberList,
].includes(this.state.phase);
const isPhaseRoom = [
];
const roomPhases = [
RightPanel.Phase.GroupRoomList,
RightPanel.Phase.GroupRoomInfo,
].includes(this.state.phase);
];
return [
<HeaderButton key="_groupMembersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
isHighlighted={isPhaseGroup}
isHighlighted={this.isPhase(groupPhases)}
clickPhase={RightPanel.Phase.GroupMemberList}
analytics={['Right Panel', 'Group Member List Button', 'click']}
/>,
<HeaderButton key="_roomsButton" title={_t('Rooms')} iconSrc="img/icons-room-nobg.svg"
isHighlighted={isPhaseRoom}
isHighlighted={this.isPhase(roomPhases)}
clickPhase={RightPanel.Phase.GroupRoomList}
analytics={['Right Panel', 'Group Room List Button', 'click']}
/>,

View File

@ -36,6 +36,7 @@ export default class HeaderButton extends React.Component {
dis.dispatch({
action: 'view_right_panel_phase',
phase: this.props.clickPhase,
fromHeader: true,
});
}

View File

@ -18,6 +18,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
export default class HeaderButtons extends React.Component {
@ -25,7 +26,7 @@ export default class HeaderButtons extends React.Component {
super(props);
this.state = {
phase: initialPhase,
phase: props.collapsedRhs ? null : initialPhase,
isUserPrivilegedInGroup: null,
};
this.onAction = this.onAction.bind(this);
@ -47,11 +48,42 @@ export default class HeaderButtons extends React.Component {
}, extras));
}
isPhase(phases) {
if (this.props.collapsedRhs) {
return false;
}
if (Array.isArray(phases)) {
return phases.includes(this.state.phase);
} else {
return phases === this.state.phase;
}
}
onAction(payload) {
if (payload.action === "view_right_panel_phase") {
this.setState({
phase: payload.phase,
});
// only actions coming from header buttons should collapse the right panel
if (this.state.phase === payload.phase && payload.fromHeader) {
dis.dispatch({
action: 'hide_right_panel',
});
this.setState({
phase: null,
});
} else {
if (this.props.collapsedRhs && payload.fromHeader) {
dis.dispatch({
action: 'show_right_panel',
});
// emit payload again as the RightPanel didn't exist up
// till show_right_panel, just without the fromHeader flag
// as that would hide the right panel again
dis.dispatch(Object.assign({}, payload, {fromHeader: false}));
}
this.setState({
phase: payload.phase,
});
}
}
}
@ -62,3 +94,7 @@ export default class HeaderButtons extends React.Component {
</div>;
}
}
HeaderButtons.propTypes = {
collapsedRhs: PropTypes.bool,
};

View File

@ -46,24 +46,24 @@ export default class RoomHeaderButtons extends HeaderButtons {
}
renderButtons() {
const isMembersPhase = [
const membersPhases = [
RightPanel.Phase.RoomMemberList,
RightPanel.Phase.RoomMemberInfo,
].includes(this.state.phase);
];
return [
<HeaderButton key="_membersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
isHighlighted={isMembersPhase}
<HeaderButton key="_membersButton" title={_t('Members')} iconSrc="img/feather-icons/user.svg"
isHighlighted={this.isPhase(membersPhases)}
clickPhase={RightPanel.Phase.RoomMemberList}
analytics={['Right Panel', 'Member List Button', 'click']}
/>,
<HeaderButton key="_filesButton" title={_t('Files')} iconSrc="img/icons-files.svg"
isHighlighted={this.state.phase === RightPanel.Phase.FilePanel}
<HeaderButton key="_filesButton" title={_t('Files')} iconSrc="img/feather-icons/files.svg"
isHighlighted={this.isPhase(RightPanel.Phase.FilePanel)}
clickPhase={RightPanel.Phase.FilePanel}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="_notifsButton" title={_t('Notifications')} iconSrc="img/icons-notifications.svg"
isHighlighted={this.state.phase === RightPanel.Phase.NotificationPanel}
<HeaderButton key="_notifsButton" title={_t('Notifications')} iconSrc="img/feather-icons/notifications.svg"
isHighlighted={this.isPhase(RightPanel.Phase.NotificationPanel)}
clickPhase={RightPanel.Phase.NotificationPanel}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,

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