Merge branch 'develop' into luke/rts-welcome-pages
Conflicts: src/components/views/avatars/BaseAvatar.js
This commit is contained in:
commit
acde1f3db7
153 changed files with 5544 additions and 1599 deletions
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
src/component-index.js
|
117
.eslintrc
117
.eslintrc
|
@ -1,117 +0,0 @@
|
||||||
{
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"plugins": [
|
|
||||||
"react",
|
|
||||||
"flowtype"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true,
|
|
||||||
"impliedStrict": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"amd": true,
|
|
||||||
"es6": true,
|
|
||||||
"node": true,
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"extends": ["eslint:recommended", "plugin:react/recommended"],
|
|
||||||
"rules": {
|
|
||||||
"no-undef": ["warn"],
|
|
||||||
"global-strict": ["off"],
|
|
||||||
"no-extra-semi": ["warn"],
|
|
||||||
"no-underscore-dangle": ["off"],
|
|
||||||
"no-console": ["off"],
|
|
||||||
"no-unused-vars": ["off"],
|
|
||||||
"no-trailing-spaces": ["warn", {
|
|
||||||
"skipBlankLines": true
|
|
||||||
}],
|
|
||||||
"no-unreachable": ["warn"],
|
|
||||||
"no-spaced-func": ["warn"],
|
|
||||||
"no-new-func": ["error"],
|
|
||||||
"no-new-wrappers": ["error"],
|
|
||||||
"no-invalid-regexp": ["error"],
|
|
||||||
"no-extra-bind": ["error"],
|
|
||||||
"no-magic-numbers": ["error", {
|
|
||||||
"ignore": [-1, 0, 1], // usually used in array/string indexing
|
|
||||||
"ignoreArrayIndexes": true,
|
|
||||||
"enforceConst": true,
|
|
||||||
"detectObjects": true
|
|
||||||
}],
|
|
||||||
"consistent-return": ["error"],
|
|
||||||
"valid-jsdoc": ["error"],
|
|
||||||
"no-use-before-define": ["error"],
|
|
||||||
"camelcase": ["warn"],
|
|
||||||
"array-callback-return": ["error"],
|
|
||||||
"dot-location": ["warn", "property"],
|
|
||||||
"guard-for-in": ["error"],
|
|
||||||
"no-useless-call": ["warn"],
|
|
||||||
"no-useless-escape": ["warn"],
|
|
||||||
"no-useless-concat": ["warn"],
|
|
||||||
"brace-style": ["warn", "1tbs"],
|
|
||||||
"comma-style": ["warn", "last"],
|
|
||||||
"space-before-function-paren": ["warn", "never"],
|
|
||||||
"space-before-blocks": ["warn", "always"],
|
|
||||||
"keyword-spacing": ["warn", {
|
|
||||||
"before": true,
|
|
||||||
"after": true
|
|
||||||
}],
|
|
||||||
|
|
||||||
// dangling commas required, but only for multiline objects/arrays
|
|
||||||
"comma-dangle": ["warn", "always-multiline"],
|
|
||||||
// always === instead of ==, unless dealing with null/undefined
|
|
||||||
"eqeqeq": ["error", "smart"],
|
|
||||||
// always use curly braces, even with single statements
|
|
||||||
"curly": ["error", "all"],
|
|
||||||
// phasing out var in favour of let/const is a good idea
|
|
||||||
"no-var": ["warn"],
|
|
||||||
// always require semicolons
|
|
||||||
"semi": ["error", "always"],
|
|
||||||
// prefer rest and spread over the Old Ways
|
|
||||||
"prefer-spread": ["warn"],
|
|
||||||
"prefer-rest-params": ["warn"],
|
|
||||||
|
|
||||||
/** react **/
|
|
||||||
|
|
||||||
// bind or arrow function in props causes performance issues
|
|
||||||
"react/jsx-no-bind": ["error", {
|
|
||||||
"ignoreRefs": true
|
|
||||||
}],
|
|
||||||
"react/jsx-key": ["error"],
|
|
||||||
"react/prefer-stateless-function": ["warn"],
|
|
||||||
|
|
||||||
/** flowtype **/
|
|
||||||
"flowtype/require-parameter-type": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"excludeArrowFunctions": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"flowtype/define-flow-type": 1,
|
|
||||||
"flowtype/require-return-type": [
|
|
||||||
1,
|
|
||||||
"always",
|
|
||||||
{
|
|
||||||
"annotateUndefined": "never",
|
|
||||||
"excludeArrowFunctions": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"flowtype/space-after-type-colon": [
|
|
||||||
1,
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"flowtype/space-before-type-colon": [
|
|
||||||
1,
|
|
||||||
"never"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"flowtype": {
|
|
||||||
"onlyFilesWithFlowAnnotation": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
75
.eslintrc.js
Normal file
75
.eslintrc.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// get the path of the js-sdk so we can extend the config
|
||||||
|
// eslint supports loading extended configs by module,
|
||||||
|
// but only if they come from a module that starts with eslint-config-
|
||||||
|
// So we load the filename directly (and it could be in node_modules/
|
||||||
|
// or or ../node_modules/ etc)
|
||||||
|
const matrixJsSdkPath = path.dirname(require.resolve('matrix-js-sdk'));
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parser: "babel-eslint",
|
||||||
|
extends: [matrixJsSdkPath + "/.eslintrc.js"],
|
||||||
|
plugins: [
|
||||||
|
"react",
|
||||||
|
"flowtype",
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
/** react **/
|
||||||
|
// This just uses the react plugin to help eslint known when
|
||||||
|
// variables have been used in JSX
|
||||||
|
"react/jsx-uses-vars": "error",
|
||||||
|
|
||||||
|
// bind or arrow function in props causes performance issues
|
||||||
|
"react/jsx-no-bind": ["error", {
|
||||||
|
"ignoreRefs": true,
|
||||||
|
}],
|
||||||
|
"react/jsx-key": ["error"],
|
||||||
|
|
||||||
|
/** flowtype **/
|
||||||
|
"flowtype/require-parameter-type": ["warn", {
|
||||||
|
"excludeArrowFunctions": true,
|
||||||
|
}],
|
||||||
|
"flowtype/define-flow-type": "warn",
|
||||||
|
"flowtype/require-return-type": ["warn",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"annotateUndefined": "never",
|
||||||
|
"excludeArrowFunctions": true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flowtype/space-after-type-colon": ["warn", "always"],
|
||||||
|
"flowtype/space-before-type-colon": ["warn", "never"],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* things that are errors in the js-sdk config that the current
|
||||||
|
* code does not adhere to, turned down to warn
|
||||||
|
*/
|
||||||
|
"max-len": ["warn", {
|
||||||
|
// apparently people believe the length limit shouldn't apply
|
||||||
|
// to JSX.
|
||||||
|
ignorePattern: '^\\s*<',
|
||||||
|
}],
|
||||||
|
"valid-jsdoc": ["warn"],
|
||||||
|
"new-cap": ["warn"],
|
||||||
|
"key-spacing": ["warn"],
|
||||||
|
"arrow-parens": ["warn"],
|
||||||
|
"prefer-const": ["warn"],
|
||||||
|
|
||||||
|
// crashes currently: https://github.com/eslint/eslint/issues/6274
|
||||||
|
"generator-star-spacing": "off",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
flowtype: {
|
||||||
|
onlyFilesWithFlowAnnotation: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
25
.travis-test-riot.sh
Executable file
25
.travis-test-riot.sh
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# script which is run by the travis build (after `npm run test`).
|
||||||
|
#
|
||||||
|
# clones riot-web develop and runs the tests against our version of react-sdk.
|
||||||
|
|
||||||
|
set -ev
|
||||||
|
|
||||||
|
RIOT_WEB_DIR=riot-web
|
||||||
|
REACT_SDK_DIR=`pwd`
|
||||||
|
|
||||||
|
git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \
|
||||||
|
"$RIOT_WEB_DIR"
|
||||||
|
|
||||||
|
cd "$RIOT_WEB_DIR"
|
||||||
|
|
||||||
|
mkdir node_modules
|
||||||
|
npm install
|
||||||
|
|
||||||
|
(cd node_modules/matrix-js-sdk && npm install)
|
||||||
|
|
||||||
|
rm -r node_modules/matrix-react-sdk
|
||||||
|
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
|
||||||
|
|
||||||
|
npm run test
|
|
@ -1,3 +1,9 @@
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- node # Latest stable version of nodejs.
|
- node # Latest stable version of nodejs.
|
||||||
|
install:
|
||||||
|
- npm install
|
||||||
|
- (cd node_modules/matrix-js-sdk && npm install)
|
||||||
|
script:
|
||||||
|
- npm run test
|
||||||
|
- ./.travis-test-riot.sh
|
||||||
|
|
163
CHANGELOG.md
163
CHANGELOG.md
|
@ -1,3 +1,166 @@
|
||||||
|
Changes in [0.8.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.5) (2017-01-16)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.5-rc.1...v0.8.5)
|
||||||
|
|
||||||
|
* Pull in newer matrix-js-sdk for video calling fix
|
||||||
|
|
||||||
|
Changes in [0.8.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.5-rc.1) (2017-01-13)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.4...v0.8.5-rc.1)
|
||||||
|
|
||||||
|
* Build the js-sdk in the CI script
|
||||||
|
[\#612](https://github.com/matrix-org/matrix-react-sdk/pull/612)
|
||||||
|
* Fix redacted member events being visible
|
||||||
|
[\#609](https://github.com/matrix-org/matrix-react-sdk/pull/609)
|
||||||
|
* Use `getStateKey` instead of `getSender`
|
||||||
|
[\#611](https://github.com/matrix-org/matrix-react-sdk/pull/611)
|
||||||
|
* Move screen sharing error check into platform
|
||||||
|
[\#608](https://github.com/matrix-org/matrix-react-sdk/pull/608)
|
||||||
|
* Fix 'create account' link in 'forgot password'
|
||||||
|
[\#606](https://github.com/matrix-org/matrix-react-sdk/pull/606)
|
||||||
|
* Let electron users complete captchas in a web browser
|
||||||
|
[\#601](https://github.com/matrix-org/matrix-react-sdk/pull/601)
|
||||||
|
* Add support for deleting threepids
|
||||||
|
[\#597](https://github.com/matrix-org/matrix-react-sdk/pull/597)
|
||||||
|
* Display msisdn threepids as 'Phone'
|
||||||
|
[\#598](https://github.com/matrix-org/matrix-react-sdk/pull/598)
|
||||||
|
|
||||||
|
Changes in [0.8.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.4) (2016-12-24)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.3...v0.8.4)
|
||||||
|
|
||||||
|
* Fix signup by working around the fact that reCapture doesn't work on electron
|
||||||
|
* Fix windows shortcut link
|
||||||
|
|
||||||
|
Changes in [0.8.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.3) (2016-12-22)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.2...v0.8.3)
|
||||||
|
|
||||||
|
* Revert performance fix for wantsDateSeperator which was causing date separators to
|
||||||
|
be shown at the wrong time of day.
|
||||||
|
* Unbranded error messages
|
||||||
|
[\#599](https://github.com/matrix-org/matrix-react-sdk/pull/599)
|
||||||
|
* Fix scroll jumping when a video is decrypted
|
||||||
|
[\#594](https://github.com/matrix-org/matrix-react-sdk/pull/594)
|
||||||
|
|
||||||
|
Changes in [0.8.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.2) (2016-12-16)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.1...v0.8.2)
|
||||||
|
|
||||||
|
* Improve the performance of MemberEventListSummary
|
||||||
|
[\#590](https://github.com/matrix-org/matrix-react-sdk/pull/590)
|
||||||
|
* Implement bulk invite rejections
|
||||||
|
[\#592](https://github.com/matrix-org/matrix-react-sdk/pull/592)
|
||||||
|
* Fix performance issues with wantsDateSeperator
|
||||||
|
[\#591](https://github.com/matrix-org/matrix-react-sdk/pull/591)
|
||||||
|
* Add read receipt times to the hovertip of read markers
|
||||||
|
[\#586](https://github.com/matrix-org/matrix-react-sdk/pull/586)
|
||||||
|
* Don't throw exception on stop if no DMRoomMap
|
||||||
|
[\#589](https://github.com/matrix-org/matrix-react-sdk/pull/589)
|
||||||
|
* Fix failing test
|
||||||
|
[\#587](https://github.com/matrix-org/matrix-react-sdk/pull/587)
|
||||||
|
|
||||||
|
Changes in [0.8.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.1) (2016-12-09)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.1-rc.2...v0.8.1)
|
||||||
|
|
||||||
|
No changes
|
||||||
|
|
||||||
|
Changes in [0.8.1-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.1-rc.2) (2016-12-06)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.1-rc.1...v0.8.1-rc.2)
|
||||||
|
|
||||||
|
* Fix exception when clearing room dir search
|
||||||
|
[\#585](https://github.com/matrix-org/matrix-react-sdk/pull/585)
|
||||||
|
* Allow integration UI URLs with paths
|
||||||
|
[\#583](https://github.com/matrix-org/matrix-react-sdk/pull/583)
|
||||||
|
* Give the search box field a name
|
||||||
|
[\#584](https://github.com/matrix-org/matrix-react-sdk/pull/584)
|
||||||
|
* Pass the room object into displayNotification
|
||||||
|
[\#582](https://github.com/matrix-org/matrix-react-sdk/pull/582)
|
||||||
|
* Don't throw an exception entering settings page
|
||||||
|
[\#581](https://github.com/matrix-org/matrix-react-sdk/pull/581)
|
||||||
|
|
||||||
|
Changes in [0.8.1-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.1-rc.1) (2016-12-05)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.0...v0.8.1-rc.1)
|
||||||
|
|
||||||
|
* Strip (IRC) when clicking on username
|
||||||
|
[\#579](https://github.com/matrix-org/matrix-react-sdk/pull/579)
|
||||||
|
* Fix scroll jump on image decryption
|
||||||
|
[\#577](https://github.com/matrix-org/matrix-react-sdk/pull/577)
|
||||||
|
* Make cut operations update the tab complete list
|
||||||
|
[\#576](https://github.com/matrix-org/matrix-react-sdk/pull/576)
|
||||||
|
* s/block/blacklist for e2e
|
||||||
|
[\#574](https://github.com/matrix-org/matrix-react-sdk/pull/574)
|
||||||
|
* Fix the download icon on attachments
|
||||||
|
[\#573](https://github.com/matrix-org/matrix-react-sdk/pull/573)
|
||||||
|
* Don't default the page_type to room directory
|
||||||
|
[\#572](https://github.com/matrix-org/matrix-react-sdk/pull/572)
|
||||||
|
* Fix crash on logging in
|
||||||
|
[\#571](https://github.com/matrix-org/matrix-react-sdk/pull/571)
|
||||||
|
* Reinstate missing sections from the UserSettings
|
||||||
|
[\#569](https://github.com/matrix-org/matrix-react-sdk/pull/569)
|
||||||
|
* Bump browser-encrypt-attachment to v0.2.0
|
||||||
|
[\#568](https://github.com/matrix-org/matrix-react-sdk/pull/568)
|
||||||
|
* Make the unpagination process less aggressive
|
||||||
|
[\#567](https://github.com/matrix-org/matrix-react-sdk/pull/567)
|
||||||
|
* Get rid of always-on labs settings
|
||||||
|
[\#566](https://github.com/matrix-org/matrix-react-sdk/pull/566)
|
||||||
|
* Fix 'Quote' for e2e messages
|
||||||
|
[\#565](https://github.com/matrix-org/matrix-react-sdk/pull/565)
|
||||||
|
|
||||||
|
Changes in [0.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.0) (2016-11-19)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.7.5...v0.8.0)
|
||||||
|
|
||||||
|
* Fix more membership change collapsing bugs
|
||||||
|
[\#560](https://github.com/matrix-org/matrix-react-sdk/pull/560)
|
||||||
|
* Show an open padlock for unencrypted rooms
|
||||||
|
[\#557](https://github.com/matrix-org/matrix-react-sdk/pull/557)
|
||||||
|
* Clean up MFileBody.presentableTextForFile
|
||||||
|
[\#558](https://github.com/matrix-org/matrix-react-sdk/pull/558)
|
||||||
|
* Update eventtiles when the events are decrypted
|
||||||
|
[\#556](https://github.com/matrix-org/matrix-react-sdk/pull/556)
|
||||||
|
* Update EventTile to use WithMatrixClient instead of MatrixClientPeg
|
||||||
|
[\#552](https://github.com/matrix-org/matrix-react-sdk/pull/552)
|
||||||
|
* Disable conference calling for encrypted rooms
|
||||||
|
[\#549](https://github.com/matrix-org/matrix-react-sdk/pull/549)
|
||||||
|
* Encrypt attachments in encrypted rooms
|
||||||
|
[\#548](https://github.com/matrix-org/matrix-react-sdk/pull/548)
|
||||||
|
* Fix MemberAvatar PropTypes & MemberEventListSummary key
|
||||||
|
[\#547](https://github.com/matrix-org/matrix-react-sdk/pull/547)
|
||||||
|
* Revert "Encrypt attachments in encrypted rooms,"
|
||||||
|
[\#546](https://github.com/matrix-org/matrix-react-sdk/pull/546)
|
||||||
|
* Fix the vector web version in UserSettings
|
||||||
|
[\#542](https://github.com/matrix-org/matrix-react-sdk/pull/542)
|
||||||
|
* Truncate consecutive member events
|
||||||
|
[\#544](https://github.com/matrix-org/matrix-react-sdk/pull/544)
|
||||||
|
* Encrypt attachments in encrypted rooms,
|
||||||
|
[\#533](https://github.com/matrix-org/matrix-react-sdk/pull/533)
|
||||||
|
* Fix the ctrl+e mute camera shortcut
|
||||||
|
[\#545](https://github.com/matrix-org/matrix-react-sdk/pull/545)
|
||||||
|
* Show the error that occured when trying to reach scalar
|
||||||
|
[\#543](https://github.com/matrix-org/matrix-react-sdk/pull/543)
|
||||||
|
* Don't do URL previews for matrix.to
|
||||||
|
[\#541](https://github.com/matrix-org/matrix-react-sdk/pull/541)
|
||||||
|
* Fix NPE in LoggedInView
|
||||||
|
[\#540](https://github.com/matrix-org/matrix-react-sdk/pull/540)
|
||||||
|
* Make room alias & user ID links matrix.to links
|
||||||
|
[\#538](https://github.com/matrix-org/matrix-react-sdk/pull/538)
|
||||||
|
* Make MemberInfo use the matrixclient from the context
|
||||||
|
[\#537](https://github.com/matrix-org/matrix-react-sdk/pull/537)
|
||||||
|
* Add the MatrixClient to the react context
|
||||||
|
[\#536](https://github.com/matrix-org/matrix-react-sdk/pull/536)
|
||||||
|
* Factor out LoggedInView from MatrixChat
|
||||||
|
[\#535](https://github.com/matrix-org/matrix-react-sdk/pull/535)
|
||||||
|
* Move 'new version' support into Platform
|
||||||
|
[\#532](https://github.com/matrix-org/matrix-react-sdk/pull/532)
|
||||||
|
* Move Notifications into Platform
|
||||||
|
[\#534](https://github.com/matrix-org/matrix-react-sdk/pull/534)
|
||||||
|
* Move platform-specific functionality into Platform
|
||||||
|
[\#531](https://github.com/matrix-org/matrix-react-sdk/pull/531)
|
||||||
|
|
||||||
Changes in [0.7.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.7.5) (2016-11-04)
|
Changes in [0.7.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.7.5) (2016-11-04)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.7.5-rc.1...v0.7.5)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.7.5-rc.1...v0.7.5)
|
||||||
|
|
30
README.md
30
README.md
|
@ -12,17 +12,17 @@ a 'skin'. A skin provides:
|
||||||
* Zero or more 'modules' containing non-UI functionality
|
* Zero or more 'modules' containing non-UI functionality
|
||||||
|
|
||||||
**WARNING: As of July 2016, the skinning abstraction is broken due to rapid
|
**WARNING: As of July 2016, the skinning abstraction is broken due to rapid
|
||||||
development of `matrix-react-sdk` to meet the needs of Vector, the first app
|
development of `matrix-react-sdk` to meet the needs of Riot (codenamed Vector), the first app
|
||||||
to be built on top of the SDK** (https://github.com/vector-im/vector-web).
|
to be built on top of the SDK** (https://github.com/vector-im/riot-web).
|
||||||
Right now `matrix-react-sdk` depends on some functionality from `vector-web`
|
Right now `matrix-react-sdk` depends on some functionality from `riot-web`
|
||||||
(e.g. CSS), and `matrix-react-sdk` contains some Vector specific behaviour
|
(e.g. CSS), and `matrix-react-sdk` contains some Riot specific behaviour
|
||||||
(grep for 'vector'). This layering will be fixed asap once Vector development
|
(grep for 'vector'). This layering will be fixed asap once Riot development
|
||||||
has stabilised, but for now we do not advise trying to create new skins for
|
has stabilised, but for now we do not advise trying to create new skins for
|
||||||
matrix-react-sdk until the layers are clearly separated again.
|
matrix-react-sdk until the layers are clearly separated again.
|
||||||
|
|
||||||
In the interim, `vector-im/vector-web` and `matrix-org/matrix-react-sdk` should
|
In the interim, `vector-im/riot-web` and `matrix-org/matrix-react-sdk` should
|
||||||
be considered as a single project (for instance, matrix-react-sdk bugs
|
be considered as a single project (for instance, matrix-react-sdk bugs
|
||||||
are currently filed against vector-im/vector-web rather than this project).
|
are currently filed against vector-im/riot-web rather than this project).
|
||||||
|
|
||||||
Developer Guide
|
Developer Guide
|
||||||
===============
|
===============
|
||||||
|
@ -44,15 +44,15 @@ https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
|
||||||
Please follow the Matrix JS/React code style as per:
|
Please follow the Matrix JS/React code style as per:
|
||||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/code_style.rst
|
https://github.com/matrix-org/matrix-react-sdk/tree/master/code_style.rst
|
||||||
|
|
||||||
Whilst the layering separation between matrix-react-sdk and Vector is broken
|
Whilst the layering separation between matrix-react-sdk and Riot is broken
|
||||||
(as of July 2016), code should be committed as follows:
|
(as of July 2016), code should be committed as follows:
|
||||||
* All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
|
* All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
|
||||||
* Vector-specific components: https://github.com/vector-im/vector-web/tree/master/src/components
|
* Riot-specific components: https://github.com/vector-im/riot-web/tree/master/src/components
|
||||||
* In practice, `matrix-react-sdk` is still evolving so fast that the maintenance
|
* In practice, `matrix-react-sdk` is still evolving so fast that the maintenance
|
||||||
burden of customising and overriding these components for Vector can seriously
|
burden of customising and overriding these components for Riot can seriously
|
||||||
impede development. So right now, there should be very few (if any) customisations for Vector.
|
impede development. So right now, there should be very few (if any) customisations for Riot.
|
||||||
* CSS for Matrix SDK components: https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/matrix-react-sdk
|
* CSS for Matrix SDK components: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk
|
||||||
* CSS for Vector-specific overrides and components: https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/vector-web
|
* CSS for Riot-specific overrides and components: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/riot-web
|
||||||
|
|
||||||
React components in matrix-react-sdk are come in two different flavours:
|
React components in matrix-react-sdk are come in two different flavours:
|
||||||
'structures' and 'views'. Structures are stateful components which handle the
|
'structures' and 'views'. Structures are stateful components which handle the
|
||||||
|
@ -76,7 +76,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold:
|
||||||
|
|
||||||
* The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
|
* The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
|
||||||
CSS for matrix-react-sdk currently resides in
|
CSS for matrix-react-sdk currently resides in
|
||||||
https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/matrix-react-sdk.
|
https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk.
|
||||||
|
|
||||||
* Per-view CSS is optional - it could choose to inherit all its styling from
|
* Per-view CSS is optional - it could choose to inherit all its styling from
|
||||||
the context of the rest of the app, although this is unusual for any but
|
the context of the rest of the app, although this is unusual for any but
|
||||||
|
@ -129,7 +129,7 @@ from it.
|
||||||
Github Issues
|
Github Issues
|
||||||
=============
|
=============
|
||||||
|
|
||||||
All issues should be filed under https://github.com/vector-im/vector-web/issues
|
All issues should be filed under https://github.com/vector-im/riot-web/issues
|
||||||
for now.
|
for now.
|
||||||
|
|
||||||
OUTDATED: To Create Your Own Skin
|
OUTDATED: To Create Your Own Skin
|
||||||
|
|
|
@ -12,11 +12,14 @@ set -x
|
||||||
# install the other dependencies
|
# install the other dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
# we may be using a dev branch of js-sdk in which case we need to build it
|
||||||
|
(cd node_modules/matrix-js-sdk && npm install)
|
||||||
|
|
||||||
# run the mocha tests
|
# run the mocha tests
|
||||||
npm run test
|
npm run test
|
||||||
|
|
||||||
# run eslint
|
# run eslint
|
||||||
npm run lint -- -f checkstyle -o eslint.xml || true
|
npm run lintall -- -f checkstyle -o eslint.xml || true
|
||||||
|
|
||||||
# delete the old tarball, if it exists
|
# delete the old tarball, if it exists
|
||||||
rm -f matrix-react-sdk-*.tgz
|
rm -f matrix-react-sdk-*.tgz
|
||||||
|
|
|
@ -165,6 +165,14 @@ module.exports = function (config) {
|
||||||
},
|
},
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
webpackMiddleware: {
|
||||||
|
stats: {
|
||||||
|
// don't fill the console up with a mahoosive list of modules
|
||||||
|
chunks: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
browserNoActivityTimeout: 15000,
|
browserNoActivityTimeout: 15000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
29
package.json
29
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.7.5",
|
"version": "0.8.5",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
".eslintrc.js",
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"CONTRIBUTING.rst",
|
"CONTRIBUTING.rst",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
|
@ -42,13 +43,16 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-runtime": "^6.11.6",
|
"babel-runtime": "^6.11.6",
|
||||||
"browser-encrypt-attachment": "^0.1.0",
|
"blueimp-canvas-to-blob": "^3.5.0",
|
||||||
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
|
"commonmark": "^0.27.0",
|
||||||
"draft-js": "^0.8.1",
|
"draft-js": "^0.8.1",
|
||||||
"draft-js-export-html": "^0.4.0",
|
"draft-js-export-html": "^0.5.0",
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.2.0",
|
||||||
"emojione": "2.2.3",
|
"emojione": "2.2.3",
|
||||||
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "^3.1.2",
|
"filesize": "^3.1.2",
|
||||||
"flux": "^2.0.3",
|
"flux": "^2.0.3",
|
||||||
"fuse.js": "^2.2.0",
|
"fuse.js": "^2.2.0",
|
||||||
|
@ -57,22 +61,22 @@
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"marked": "^0.3.5",
|
|
||||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
"react": "^15.2.1",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "^15.2.1",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
"react-dom": "^15.2.1",
|
"react-dom": "^15.4.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||||
"sanitize-html": "^1.11.1",
|
"sanitize-html": "^1.11.1",
|
||||||
|
"text-encoding-utf-8": "^1.0.1",
|
||||||
"velocity-vector": "vector-im/velocity#059e3b2",
|
"velocity-vector": "vector-im/velocity#059e3b2",
|
||||||
"whatwg-fetch": "^1.0.0"
|
"whatwg-fetch": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.5.2",
|
"babel-cli": "^6.5.2",
|
||||||
"babel-core": "^6.14.0",
|
"babel-core": "^6.14.0",
|
||||||
"babel-eslint": "^6.1.0",
|
"babel-eslint": "^6.1.2",
|
||||||
"babel-loader": "^6.2.5",
|
"babel-loader": "^6.2.5",
|
||||||
"babel-plugin-add-module-exports": "^0.2.1",
|
"babel-plugin-add-module-exports": "^0.2.1",
|
||||||
"babel-plugin-transform-async-to-generator": "^6.16.0",
|
"babel-plugin-transform-async-to-generator": "^6.16.0",
|
||||||
|
@ -84,9 +88,10 @@
|
||||||
"babel-preset-es2016": "^6.11.3",
|
"babel-preset-es2016": "^6.11.3",
|
||||||
"babel-preset-es2017": "^6.14.0",
|
"babel-preset-es2017": "^6.14.0",
|
||||||
"babel-preset-react": "^6.11.1",
|
"babel-preset-react": "^6.11.1",
|
||||||
"eslint": "^2.13.1",
|
"eslint": "^3.13.1",
|
||||||
"eslint-plugin-flowtype": "^2.17.0",
|
"eslint-config-google": "^0.7.1",
|
||||||
"eslint-plugin-react": "^6.2.1",
|
"eslint-plugin-flowtype": "^2.30.0",
|
||||||
|
"eslint-plugin-react": "^6.9.0",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^0.13.22",
|
"karma": "^0.13.22",
|
||||||
|
@ -99,7 +104,7 @@
|
||||||
"karma-webpack": "^1.7.0",
|
"karma-webpack": "^1.7.0",
|
||||||
"mocha": "^2.4.5",
|
"mocha": "^2.4.5",
|
||||||
"phantomjs-prebuilt": "^2.1.7",
|
"phantomjs-prebuilt": "^2.1.7",
|
||||||
"react-addons-test-utils": "^15.0.1",
|
"react-addons-test-utils": "^15.4.0",
|
||||||
"require-json": "0.0.1",
|
"require-json": "0.0.1",
|
||||||
"rimraf": "^2.4.3",
|
"rimraf": "^2.4.3",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^1.17.3",
|
||||||
|
|
|
@ -49,12 +49,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultAvatarUrlForString: function(s) {
|
defaultAvatarUrlForString: function(s) {
|
||||||
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
|
var images = ['76cfa6', '50e2c2', 'f4c371'];
|
||||||
var total = 0;
|
var total = 0;
|
||||||
for (var i = 0; i < s.length; ++i) {
|
for (var i = 0; i < s.length; ++i) {
|
||||||
total += s.charCodeAt(i);
|
total += s.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return 'img/' + images[total % images.length] + '.png';
|
return 'img/' + images[total % images.length] + '.png';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default class BasePlatform {
|
||||||
* Returns true if the platform supports displaying
|
* Returns true if the platform supports displaying
|
||||||
* notifications, otherwise false.
|
* notifications, otherwise false.
|
||||||
*/
|
*/
|
||||||
supportsNotifications() : boolean {
|
supportsNotifications(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ export default class BasePlatform {
|
||||||
* Returns true if the application currently has permission
|
* Returns true if the application currently has permission
|
||||||
* to display notifications. Otherwise false.
|
* to display notifications. Otherwise false.
|
||||||
*/
|
*/
|
||||||
maySendNotifications() : boolean {
|
maySendNotifications(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,10 +60,10 @@ export default class BasePlatform {
|
||||||
* that is 'granted' if the user allowed the request or
|
* that is 'granted' if the user allowed the request or
|
||||||
* 'denied' otherwise.
|
* 'denied' otherwise.
|
||||||
*/
|
*/
|
||||||
requestNotificationPermission() : Promise<string> {
|
requestNotificationPermission(): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
displayNotification(title: string, msg: string, avatarUrl: string) {
|
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,4 +73,13 @@ export default class BasePlatform {
|
||||||
getAppVersion() {
|
getAppVersion() {
|
||||||
throw new Error("getAppVersion not implemented!");
|
throw new Error("getAppVersion not implemented!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If it's not expected that capturing the screen will work
|
||||||
|
* with getUserMedia, return a string explaining why not.
|
||||||
|
* Otherwise, return null.
|
||||||
|
*/
|
||||||
|
screenCaptureErrorString() {
|
||||||
|
return "Not implemented";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
var PlatformPeg = require("./PlatformPeg");
|
||||||
var Modal = require('./Modal');
|
var Modal = require('./Modal');
|
||||||
var sdk = require('./index');
|
var sdk = require('./index');
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
|
@ -158,10 +159,10 @@ function _setCallState(call, roomId, status) {
|
||||||
calls[roomId] = call;
|
calls[roomId] = call;
|
||||||
|
|
||||||
if (status === "ringing") {
|
if (status === "ringing") {
|
||||||
play("ringAudio")
|
play("ringAudio");
|
||||||
}
|
}
|
||||||
else if (call && call.call_state === "ringing") {
|
else if (call && call.call_state === "ringing") {
|
||||||
pause("ringAudio")
|
pause("ringAudio");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (call) {
|
if (call) {
|
||||||
|
@ -187,6 +188,17 @@ function _onAction(payload) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else if (payload.type === 'screensharing') {
|
else if (payload.type === 'screensharing') {
|
||||||
|
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||||
|
if (screenCapErrorString) {
|
||||||
|
_setCallState(undefined, newCall.roomId, "ended");
|
||||||
|
console.log("Can't capture screen: " + screenCapErrorString);
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Unable to capture screen",
|
||||||
|
description: screenCapErrorString
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
newCall.placeScreenSharingCall(
|
newCall.placeScreenSharingCall(
|
||||||
payload.remote_element,
|
payload.remote_element,
|
||||||
payload.local_element
|
payload.local_element
|
||||||
|
@ -280,7 +292,7 @@ function _onAction(payload) {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Warning!",
|
title: "Warning!",
|
||||||
description: "Conference calling in Riot is in development and may not be reliable.",
|
description: "Conference calling is in development and may not be reliable.",
|
||||||
onFinished: confirm=>{
|
onFinished: confirm=>{
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
ConferenceHandler.createNewMatrixCall(
|
ConferenceHandler.createNewMatrixCall(
|
||||||
|
|
|
@ -25,22 +25,87 @@ var Modal = require('./Modal');
|
||||||
|
|
||||||
var encrypt = require("browser-encrypt-attachment");
|
var encrypt = require("browser-encrypt-attachment");
|
||||||
|
|
||||||
function infoForImageFile(imageFile) {
|
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||||
var deferred = q.defer();
|
require("blueimp-canvas-to-blob");
|
||||||
|
|
||||||
|
const MAX_WIDTH = 800;
|
||||||
|
const MAX_HEIGHT = 600;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a thumbnail for a image DOM element.
|
||||||
|
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
|
||||||
|
* The thumbnail will have the same aspect ratio as the original.
|
||||||
|
* Draws the element into a canvas using CanvasRenderingContext2D.drawImage
|
||||||
|
* Then calls Canvas.toBlob to get a blob object for the image data.
|
||||||
|
*
|
||||||
|
* Since it needs to calculate the dimensions of the source image and the
|
||||||
|
* thumbnailed image it returns an info object filled out with information
|
||||||
|
* about the original image and the thumbnail.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element The element to thumbnail.
|
||||||
|
* @param {integer} inputWidth The width of the image in the input element.
|
||||||
|
* @param {integer} inputHeight the width of the image in the input element.
|
||||||
|
* @param {String} mimeType The mimeType to save the blob as.
|
||||||
|
* @return {Promise} A promise that resolves with an object with an info key
|
||||||
|
* and a thumbnail key.
|
||||||
|
*/
|
||||||
|
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||||
|
const deferred = q.defer();
|
||||||
|
|
||||||
|
var targetWidth = inputWidth;
|
||||||
|
var targetHeight = inputHeight;
|
||||||
|
if (targetHeight > MAX_HEIGHT) {
|
||||||
|
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
|
||||||
|
targetHeight = MAX_HEIGHT;
|
||||||
|
}
|
||||||
|
if (targetWidth > MAX_WIDTH) {
|
||||||
|
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
|
||||||
|
targetWidth = MAX_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = targetWidth;
|
||||||
|
canvas.height = targetHeight;
|
||||||
|
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
|
||||||
|
canvas.toBlob(function(thumbnail) {
|
||||||
|
deferred.resolve({
|
||||||
|
info: {
|
||||||
|
thumbnail_info: {
|
||||||
|
w: targetWidth,
|
||||||
|
h: targetHeight,
|
||||||
|
mimetype: thumbnail.type,
|
||||||
|
size: thumbnail.size,
|
||||||
|
},
|
||||||
|
w: inputWidth,
|
||||||
|
h: inputHeight,
|
||||||
|
},
|
||||||
|
thumbnail: thumbnail
|
||||||
|
});
|
||||||
|
}, mimeType);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a file into a newly created image element.
|
||||||
|
*
|
||||||
|
* @param {File} file The file to load in an image element.
|
||||||
|
* @return {Promise} A promise that resolves with the html image element.
|
||||||
|
*/
|
||||||
|
function loadImageElement(imageFile) {
|
||||||
|
const deferred = q.defer();
|
||||||
|
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
var img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
|
|
||||||
var reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
img.src = e.target.result;
|
img.src = e.target.result;
|
||||||
|
|
||||||
// Once ready, returns its size
|
// Once ready, create a thumbnail
|
||||||
img.onload = function() {
|
img.onload = function() {
|
||||||
deferred.resolve({
|
deferred.resolve(img);
|
||||||
w: img.width,
|
|
||||||
h: img.height
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
img.onerror = function(e) {
|
img.onerror = function(e) {
|
||||||
deferred.reject(e);
|
deferred.reject(e);
|
||||||
|
@ -54,22 +119,53 @@ function infoForImageFile(imageFile) {
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function infoForVideoFile(videoFile) {
|
/**
|
||||||
var deferred = q.defer();
|
* Read the metadata for an image file and create and upload a thumbnail of the image.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
|
||||||
|
* @param {String} roomId The ID of the room the image will be uploaded in.
|
||||||
|
* @param {File} The image to read and thumbnail.
|
||||||
|
* @return {Promise} A promise that resolves with the attachment info.
|
||||||
|
*/
|
||||||
|
function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||||
|
var thumbnailType = "image/png";
|
||||||
|
if (imageFile.type == "image/jpeg") {
|
||||||
|
thumbnailType = "image/jpeg";
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageInfo;
|
||||||
|
return loadImageElement(imageFile).then(function(img) {
|
||||||
|
return createThumbnail(img, img.width, img.height, thumbnailType);
|
||||||
|
}).then(function(result) {
|
||||||
|
imageInfo = result.info;
|
||||||
|
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
|
}).then(function(result) {
|
||||||
|
imageInfo.thumbnail_url = result.url;
|
||||||
|
imageInfo.thumbnail_file = result.file;
|
||||||
|
return imageInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a file into a newly created video element.
|
||||||
|
*
|
||||||
|
* @param {File} file The file to load in an video element.
|
||||||
|
* @return {Promise} A promise that resolves with the video image element.
|
||||||
|
*/
|
||||||
|
function loadVideoElement(videoFile) {
|
||||||
|
const deferred = q.defer();
|
||||||
|
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
var video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
|
|
||||||
var reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
video.src = e.target.result;
|
video.src = e.target.result;
|
||||||
|
|
||||||
// Once ready, returns its size
|
// Once ready, returns its size
|
||||||
video.onloadedmetadata = function() {
|
// Wait until we have enough data to thumbnail the first frame.
|
||||||
deferred.resolve({
|
video.onloadeddata = function() {
|
||||||
w: video.videoWidth,
|
deferred.resolve(video);
|
||||||
h: video.videoHeight
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
video.onerror = function(e) {
|
video.onerror = function(e) {
|
||||||
deferred.reject(e);
|
deferred.reject(e);
|
||||||
|
@ -83,6 +179,30 @@ function infoForVideoFile(videoFile) {
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the metadata for a video file and create and upload a thumbnail of the video.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
|
||||||
|
* @param {String} roomId The ID of the room the video will be uploaded to.
|
||||||
|
* @param {File} The video to read and thumbnail.
|
||||||
|
* @return {Promise} A promise that resolves with the attachment info.
|
||||||
|
*/
|
||||||
|
function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||||
|
const thumbnailType = "image/jpeg";
|
||||||
|
|
||||||
|
var videoInfo;
|
||||||
|
return loadVideoElement(videoFile).then(function(video) {
|
||||||
|
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
||||||
|
}).then(function(result) {
|
||||||
|
videoInfo = result.info;
|
||||||
|
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
|
}).then(function(result) {
|
||||||
|
videoInfo.thumbnail_url = result.url;
|
||||||
|
videoInfo.thumbnail_file = result.file;
|
||||||
|
return videoInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the file as an ArrayBuffer.
|
* Read the file as an ArrayBuffer.
|
||||||
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
|
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
|
||||||
|
@ -101,6 +221,52 @@ function readFileAsArrayBuffer(file) {
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload the file to the content repository.
|
||||||
|
* If the room is encrypted then encrypt the file before uploading.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient The matrix client to upload the file with.
|
||||||
|
* @param {String} roomId The ID of the room being uploaded to.
|
||||||
|
* @param {File} file The file to upload.
|
||||||
|
* @return {Promise} A promise that resolves with an object.
|
||||||
|
* If the file is unencrypted then the object will have a "url" key.
|
||||||
|
* If the file is encrypted then the object will have a "file" key.
|
||||||
|
*/
|
||||||
|
function uploadFile(matrixClient, roomId, file) {
|
||||||
|
if (matrixClient.isRoomEncrypted(roomId)) {
|
||||||
|
// If the room is encrypted then encrypt the file before uploading it.
|
||||||
|
// First read the file into memory.
|
||||||
|
return readFileAsArrayBuffer(file).then(function(data) {
|
||||||
|
// Then encrypt the file.
|
||||||
|
return encrypt.encryptAttachment(data);
|
||||||
|
}).then(function(encryptResult) {
|
||||||
|
// Record the information needed to decrypt the attachment.
|
||||||
|
const encryptInfo = encryptResult.info;
|
||||||
|
// Pass the encrypted data as a Blob to the uploader.
|
||||||
|
const blob = new Blob([encryptResult.data]);
|
||||||
|
return matrixClient.uploadContent(blob).then(function(url) {
|
||||||
|
// If the attachment is encrypted then bundle the URL along
|
||||||
|
// with the information needed to decrypt the attachment and
|
||||||
|
// add it under a file key.
|
||||||
|
encryptInfo.url = url;
|
||||||
|
if (file.type) {
|
||||||
|
encryptInfo.mimetype = file.type;
|
||||||
|
}
|
||||||
|
return {"file": encryptInfo};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const basePromise = matrixClient.uploadContent(file);
|
||||||
|
const promise1 = basePromise.then(function(url) {
|
||||||
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
|
return {"url": url};
|
||||||
|
});
|
||||||
|
// XXX: copy over the abort method to the new promise
|
||||||
|
promise1.abort = basePromise.abort;
|
||||||
|
return promise1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ContentMessages {
|
class ContentMessages {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -109,7 +275,7 @@ class ContentMessages {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendContentToRoom(file, roomId, matrixClient) {
|
sendContentToRoom(file, roomId, matrixClient) {
|
||||||
var content = {
|
const content = {
|
||||||
body: file.name,
|
body: file.name,
|
||||||
info: {
|
info: {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
|
@ -121,13 +287,14 @@ class ContentMessages {
|
||||||
content.info.mimetype = file.type;
|
content.info.mimetype = file.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
var def = q.defer();
|
const def = q.defer();
|
||||||
if (file.type.indexOf('image/') == 0) {
|
if (file.type.indexOf('image/') == 0) {
|
||||||
content.msgtype = 'm.image';
|
content.msgtype = 'm.image';
|
||||||
infoForImageFile(file).then(imageInfo=>{
|
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
|
||||||
extend(content.info, imageInfo);
|
extend(content.info, imageInfo);
|
||||||
def.resolve();
|
def.resolve();
|
||||||
}, error=>{
|
}, error=>{
|
||||||
|
console.error(error);
|
||||||
content.msgtype = 'm.file';
|
content.msgtype = 'm.file';
|
||||||
def.resolve();
|
def.resolve();
|
||||||
});
|
});
|
||||||
|
@ -136,7 +303,7 @@ class ContentMessages {
|
||||||
def.resolve();
|
def.resolve();
|
||||||
} else if (file.type.indexOf('video/') == 0) {
|
} else if (file.type.indexOf('video/') == 0) {
|
||||||
content.msgtype = 'm.video';
|
content.msgtype = 'm.video';
|
||||||
infoForVideoFile(file).then(videoInfo=>{
|
infoForVideoFile(matrixClient, roomId, file).then(videoInfo=>{
|
||||||
extend(content.info, videoInfo);
|
extend(content.info, videoInfo);
|
||||||
def.resolve();
|
def.resolve();
|
||||||
}, error=>{
|
}, error=>{
|
||||||
|
@ -148,36 +315,27 @@ class ContentMessages {
|
||||||
def.resolve();
|
def.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
var upload = {
|
const upload = {
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
total: 0,
|
total: 0,
|
||||||
loaded: 0
|
loaded: 0,
|
||||||
};
|
};
|
||||||
this.inprogress.push(upload);
|
this.inprogress.push(upload);
|
||||||
dis.dispatch({action: 'upload_started'});
|
dis.dispatch({action: 'upload_started'});
|
||||||
|
|
||||||
var encryptInfo = null;
|
|
||||||
var error;
|
var error;
|
||||||
var self = this;
|
|
||||||
return def.promise.then(function() {
|
return def.promise.then(function() {
|
||||||
if (matrixClient.isRoomEncrypted(roomId)) {
|
// XXX: upload.promise must be the promise that
|
||||||
// If the room is encrypted then encrypt the file before uploading it.
|
// is returned by uploadFile as it has an abort()
|
||||||
// First read the file into memory.
|
// method hacked onto it.
|
||||||
upload.promise = readFileAsArrayBuffer(file).then(function(data) {
|
upload.promise = uploadFile(
|
||||||
// Then encrypt the file.
|
matrixClient, roomId, file
|
||||||
return encrypt.encryptAttachment(data);
|
);
|
||||||
}).then(function(encryptResult) {
|
return upload.promise.then(function(result) {
|
||||||
// Record the information needed to decrypt the attachment.
|
content.file = result.file;
|
||||||
encryptInfo = encryptResult.info;
|
content.url = result.url;
|
||||||
// Pass the encrypted data as a Blob to the uploader.
|
});
|
||||||
var blob = new Blob([encryptResult.data]);
|
|
||||||
return matrixClient.uploadContent(blob);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
upload.promise = matrixClient.uploadContent(file);
|
|
||||||
}
|
|
||||||
return upload.promise;
|
|
||||||
}).progress(function(ev) {
|
}).progress(function(ev) {
|
||||||
if (ev) {
|
if (ev) {
|
||||||
upload.total = ev.total;
|
upload.total = ev.total;
|
||||||
|
@ -185,19 +343,6 @@ class ContentMessages {
|
||||||
dis.dispatch({action: 'upload_progress', upload: upload});
|
dis.dispatch({action: 'upload_progress', upload: upload});
|
||||||
}
|
}
|
||||||
}).then(function(url) {
|
}).then(function(url) {
|
||||||
if (encryptInfo === null) {
|
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
|
||||||
content.url = url;
|
|
||||||
} else {
|
|
||||||
// If the attachment is encrypted then bundle the URL along
|
|
||||||
// with the information needed to decrypt the attachment and
|
|
||||||
// add it under a file key.
|
|
||||||
encryptInfo.url = url;
|
|
||||||
if (file.type) {
|
|
||||||
encryptInfo.mimetype = file.type;
|
|
||||||
}
|
|
||||||
content.file = encryptInfo;
|
|
||||||
}
|
|
||||||
return matrixClient.sendMessage(roomId, content);
|
return matrixClient.sendMessage(roomId, content);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
error = err;
|
error = err;
|
||||||
|
@ -212,12 +357,12 @@ class ContentMessages {
|
||||||
description: desc
|
description: desc
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).finally(function() {
|
}).finally(() => {
|
||||||
var inprogressKeys = Object.keys(self.inprogress);
|
const inprogressKeys = Object.keys(this.inprogress);
|
||||||
for (var i = 0; i < self.inprogress.length; ++i) {
|
for (var i = 0; i < this.inprogress.length; ++i) {
|
||||||
var k = inprogressKeys[i];
|
var k = inprogressKeys[i];
|
||||||
if (self.inprogress[k].promise === upload.promise) {
|
if (this.inprogress[k].promise === upload.promise) {
|
||||||
self.inprogress.splice(k, 1);
|
this.inprogress.splice(k, 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,7 +380,7 @@ class ContentMessages {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelUpload(promise) {
|
cancelUpload(promise) {
|
||||||
var inprogressKeys = Object.keys(this.inprogress);
|
const inprogressKeys = Object.keys(this.inprogress);
|
||||||
var upload;
|
var upload;
|
||||||
for (var i = 0; i < this.inprogress.length; ++i) {
|
for (var i = 0; i < this.inprogress.length; ++i) {
|
||||||
var k = inprogressKeys[i];
|
var k = inprogressKeys[i];
|
||||||
|
|
|
@ -48,5 +48,5 @@ module.exports = {
|
||||||
//return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
//return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2);
|
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -136,6 +136,6 @@ module.exports = {
|
||||||
fromUsers: function(users, showInviteButton, inviteFn) {
|
fromUsers: function(users, showInviteButton, inviteFn) {
|
||||||
return users.map(function(u) {
|
return users.map(function(u) {
|
||||||
return new UserEntity(u, showInviteButton, inviteFn);
|
return new UserEntity(u, showInviteButton, inviteFn);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -91,16 +91,16 @@ var sanitizeHtmlParams = {
|
||||||
],
|
],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
// custom ones first:
|
// custom ones first:
|
||||||
font: [ 'color' ], // custom to matrix
|
font: ['color'], // custom to matrix
|
||||||
a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix
|
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||||
// We don't currently allow img itself by default, but this
|
// We don't currently allow img itself by default, but this
|
||||||
// would make sense if we did
|
// would make sense if we did
|
||||||
img: [ 'src' ],
|
img: ['src'],
|
||||||
},
|
},
|
||||||
// Lots of these won't come up by default because we don't allow them
|
// Lots of these won't come up by default because we don't allow them
|
||||||
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
|
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||||
// URL schemes we permit
|
// URL schemes we permit
|
||||||
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
|
||||||
|
|
||||||
// DO NOT USE. sanitize-html allows all URL starting with '//'
|
// DO NOT USE. sanitize-html allows all URL starting with '//'
|
||||||
// so this will always allow links to whatever scheme the
|
// so this will always allow links to whatever scheme the
|
||||||
|
|
|
@ -53,5 +53,5 @@ module.exports = {
|
||||||
return Math.floor(heightMulti * fullHeight);
|
return Math.floor(heightMulti * fullHeight);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,12 @@ import MultiInviter from './utils/MultiInviter';
|
||||||
|
|
||||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
|
|
||||||
|
// We allow localhost for mxids to avoid confusion
|
||||||
|
const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/
|
||||||
|
|
||||||
export function getAddressType(inputText) {
|
export function getAddressType(inputText) {
|
||||||
const isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
|
const isEmailAddress = emailRegex.test(inputText);
|
||||||
const isMatrixId = inputText[0] === '@' && inputText.indexOf(":") > 0;
|
const isMatrixId = mxidRegex.test(inputText);
|
||||||
|
|
||||||
// sanity check the input for user IDs
|
// sanity check the input for user IDs
|
||||||
if (isEmailAddress) {
|
if (isEmailAddress) {
|
||||||
|
@ -55,29 +58,7 @@ export function inviteToRoom(roomId, addr) {
|
||||||
* @returns Promise
|
* @returns Promise
|
||||||
*/
|
*/
|
||||||
export function inviteMultipleToRoom(roomId, addrs) {
|
export function inviteMultipleToRoom(roomId, addrs) {
|
||||||
this.inviter = new MultiInviter(roomId);
|
const inviter = new MultiInviter(roomId);
|
||||||
return this.inviter.invite(addrs);
|
return inviter.invite(addrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks is the supplied address is valid
|
|
||||||
*
|
|
||||||
* @param {addr} The mx userId or email address to check
|
|
||||||
* @returns true, false, or null for unsure
|
|
||||||
*/
|
|
||||||
export function isValidAddress(addr) {
|
|
||||||
// Check if the addr is a valid type
|
|
||||||
var addrType = this.getAddressType(addr);
|
|
||||||
if (addrType === "mx") {
|
|
||||||
let user = MatrixClientPeg.get().getUser(addr);
|
|
||||||
if (user) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else if (addrType === "email") {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ module.exports = {
|
||||||
TAB: 9,
|
TAB: 9,
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
SHIFT: 16,
|
SHIFT: 16,
|
||||||
|
ESCAPE: 27,
|
||||||
PAGE_UP: 33,
|
PAGE_UP: 33,
|
||||||
PAGE_DOWN: 34,
|
PAGE_DOWN: 34,
|
||||||
END: 35,
|
END: 35,
|
||||||
|
|
|
@ -18,7 +18,7 @@ import q from 'q';
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import Notifier from './Notifier'
|
import Notifier from './Notifier';
|
||||||
import UserActivity from './UserActivity';
|
import UserActivity from './UserActivity';
|
||||||
import Presence from './Presence';
|
import Presence from './Presence';
|
||||||
import dis from './dispatcher';
|
import dis from './dispatcher';
|
||||||
|
@ -140,7 +140,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
|
||||||
homeserverUrl: queryParams.homeserver,
|
homeserverUrl: queryParams.homeserver,
|
||||||
identityServerUrl: queryParams.identityServer,
|
identityServerUrl: queryParams.identityServer,
|
||||||
guest: false,
|
guest: false,
|
||||||
})
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error("Failed to log in with login token: " + err + " " +
|
console.error("Failed to log in with login token: " + err + " " +
|
||||||
err.data);
|
err.data);
|
||||||
|
@ -356,7 +356,7 @@ export function stopMatrixClient() {
|
||||||
Notifier.stop();
|
Notifier.stop();
|
||||||
UserActivity.stop();
|
UserActivity.stop();
|
||||||
Presence.stop();
|
Presence.stop();
|
||||||
DMRoomMap.shared().stop();
|
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.stopClient();
|
cli.stopClient();
|
||||||
|
|
136
src/Markdown.js
136
src/Markdown.js
|
@ -14,20 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import marked from 'marked';
|
import commonmark from 'commonmark';
|
||||||
|
|
||||||
// marked only applies the default options on the high
|
|
||||||
// level marked() interface, so we do it here.
|
|
||||||
const marked_options = Object.assign({}, marked.defaults, {
|
|
||||||
gfm: true,
|
|
||||||
tables: true,
|
|
||||||
breaks: true,
|
|
||||||
pedantic: false,
|
|
||||||
sanitize: true,
|
|
||||||
smartLists: true,
|
|
||||||
smartypants: false,
|
|
||||||
xhtml: true, // return self closing tags (ie. <br /> not <br>)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that wraps marked, adding the ability to see whether
|
* Class that wraps marked, adding the ability to see whether
|
||||||
|
@ -36,16 +23,9 @@ const marked_options = Object.assign({}, marked.defaults, {
|
||||||
*/
|
*/
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
const lexer = new marked.Lexer(marked_options);
|
this.input = input;
|
||||||
this.tokens = lexer.lex(input);
|
this.parser = new commonmark.Parser();
|
||||||
}
|
this.renderer = new commonmark.HtmlRenderer({safe: false});
|
||||||
|
|
||||||
_copyTokens() {
|
|
||||||
// copy tokens (the parser modifies its input arg)
|
|
||||||
const tokens_copy = this.tokens.slice();
|
|
||||||
// it also has a 'links' property, because this is javascript
|
|
||||||
// and why wouldn't you have an array that also has properties?
|
|
||||||
return Object.assign(tokens_copy, this.tokens);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlainText() {
|
isPlainText() {
|
||||||
|
@ -64,65 +44,81 @@ export default class Markdown {
|
||||||
is_plain = false;
|
is_plain = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dummy_renderer = {};
|
const dummy_renderer = new commonmark.HtmlRenderer();
|
||||||
for (const k of Object.keys(marked.Renderer.prototype)) {
|
for (const k of Object.keys(commonmark.HtmlRenderer.prototype)) {
|
||||||
dummy_renderer[k] = setNotPlain;
|
dummy_renderer[k] = setNotPlain;
|
||||||
}
|
}
|
||||||
// text and paragraph are just text
|
// text and paragraph are just text
|
||||||
dummy_renderer.text = function(t){return t;}
|
dummy_renderer.text = function(t) { return t; };
|
||||||
dummy_renderer.paragraph = function(t){return t;}
|
dummy_renderer.softbreak = function(t) { return t; };
|
||||||
|
dummy_renderer.paragraph = function(t) { return t; };
|
||||||
|
|
||||||
// ignore links where text is just the url:
|
const dummy_parser = new commonmark.Parser();
|
||||||
// this ignores plain URLs that markdown has
|
dummy_renderer.render(dummy_parser.parse(this.input));
|
||||||
// detected whilst preserving markdown syntax links
|
|
||||||
dummy_renderer.link = function(href, title, text) {
|
|
||||||
if (text != href) {
|
|
||||||
is_plain = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dummy_options = Object.assign({}, marked_options, {
|
|
||||||
renderer: dummy_renderer,
|
|
||||||
});
|
|
||||||
const dummy_parser = new marked.Parser(dummy_options);
|
|
||||||
dummy_parser.parse(this._copyTokens());
|
|
||||||
|
|
||||||
return is_plain;
|
return is_plain;
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML() {
|
toHTML() {
|
||||||
const real_renderer = new marked.Renderer();
|
const real_paragraph = this.renderer.paragraph;
|
||||||
real_renderer.link = function(href, title, text) {
|
|
||||||
// prevent marked from turning plain URLs
|
|
||||||
// into links, because its algorithm is fairly
|
|
||||||
// poor. Let's send plain URLs rather than
|
|
||||||
// badly linkified ones (the linkifier Vector
|
|
||||||
// uses on message display is way better, eg.
|
|
||||||
// handles URLs with closing parens at the end).
|
|
||||||
if (text == href) {
|
|
||||||
return href;
|
|
||||||
}
|
|
||||||
return marked.Renderer.prototype.link.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
real_renderer.paragraph = (text) => {
|
this.renderer.paragraph = function(node, entering) {
|
||||||
// The tokens at the top level are the 'blocks', so if we
|
// If there is only one top level node, just return the
|
||||||
// have more than one, there are multiple 'paragraphs'.
|
|
||||||
// If there is only one top level token, just return the
|
|
||||||
// bare text: it's a single line of text and so should be
|
// bare text: it's a single line of text and so should be
|
||||||
// 'inline', rather than necessarily wrapped in its own
|
// 'inline', rather than unnecessarily wrapped in its own
|
||||||
// p tag. If, however, we have multiple tokens, each gets
|
// p tag. If, however, we have multiple nodes, each gets
|
||||||
// its own p tag to keep them as separate paragraphs.
|
// its own p tag to keep them as separate paragraphs.
|
||||||
if (this.tokens.length == 1) {
|
var par = node;
|
||||||
return text;
|
while (par.parent) {
|
||||||
|
par = par.parent;
|
||||||
}
|
}
|
||||||
return '<p>' + text + '</p>';
|
if (par.firstChild != par.lastChild) {
|
||||||
}
|
real_paragraph.call(this, node, entering);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const real_options = Object.assign({}, marked_options, {
|
var parsed = this.parser.parse(this.input);
|
||||||
renderer: real_renderer,
|
var rendered = this.renderer.render(parsed);
|
||||||
});
|
|
||||||
const real_parser = new marked.Parser(real_options);
|
this.renderer.paragraph = real_paragraph;
|
||||||
return real_parser.parse(this._copyTokens());
|
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
toPlaintext() {
|
||||||
|
const real_paragraph = this.renderer.paragraph;
|
||||||
|
|
||||||
|
// The default `out` function only sends the input through an XML
|
||||||
|
// escaping function, which causes messages to be entity encoded,
|
||||||
|
// which we don't want in this case.
|
||||||
|
this.renderer.out = function(s) {
|
||||||
|
// The `lit` function adds a string literal to the output buffer.
|
||||||
|
this.lit(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.renderer.paragraph = function(node, entering) {
|
||||||
|
// If there is only one top level node, just return the
|
||||||
|
// bare text: it's a single line of text and so should be
|
||||||
|
// 'inline', rather than unnecessarily wrapped in its own
|
||||||
|
// p tag. If, however, we have multiple nodes, each gets
|
||||||
|
// its own p tag to keep them as separate paragraphs.
|
||||||
|
var par = node;
|
||||||
|
while (par.parent) {
|
||||||
|
node = par;
|
||||||
|
par = par.parent;
|
||||||
|
}
|
||||||
|
if (node != par.lastChild) {
|
||||||
|
if (!entering) {
|
||||||
|
this.lit('\n\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var parsed = this.parser.parse(this.input);
|
||||||
|
var rendered = this.renderer.render(parsed);
|
||||||
|
|
||||||
|
this.renderer.paragraph = real_paragraph;
|
||||||
|
|
||||||
|
return rendered;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
160
src/Modal.js
160
src/Modal.js
|
@ -19,44 +19,174 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require('react-dom');
|
var ReactDOM = require('react-dom');
|
||||||
|
import sdk from './index';
|
||||||
|
|
||||||
module.exports = {
|
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||||
DialogContainerId: "mx_Dialog_Container",
|
|
||||||
|
|
||||||
getOrCreateContainer: function() {
|
/**
|
||||||
var container = document.getElementById(this.DialogContainerId);
|
* Wrap an asynchronous loader function with a react component which shows a
|
||||||
|
* spinner until the real component loads.
|
||||||
|
*/
|
||||||
|
const AsyncWrapper = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
/** A function which takes a 'callback' argument which it will call
|
||||||
|
* with the real component once it loads.
|
||||||
|
*/
|
||||||
|
loader: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
component: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
|
this.props.loader((e) => {
|
||||||
|
if (this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({component: e});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const {loader, ...otherProps} = this.props;
|
||||||
|
|
||||||
|
if (this.state.component) {
|
||||||
|
const Component = this.state.component;
|
||||||
|
return <Component {...otherProps} />;
|
||||||
|
} else {
|
||||||
|
// show a spinner until the component is loaded.
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class ModalManager {
|
||||||
|
constructor() {
|
||||||
|
this._counter = 0;
|
||||||
|
|
||||||
|
/** list of the modals we have stacked up, with the most recent at [0] */
|
||||||
|
this._modals = [
|
||||||
|
/* {
|
||||||
|
elem: React component for this dialog
|
||||||
|
onFinished: caller-supplied onFinished callback
|
||||||
|
className: CSS class for the dialog wrapper div
|
||||||
|
} */
|
||||||
|
];
|
||||||
|
|
||||||
|
this.closeAll = this.closeAll.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrCreateContainer() {
|
||||||
|
let container = document.getElementById(DIALOG_CONTAINER_ID);
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.createElement("div");
|
container = document.createElement("div");
|
||||||
container.id = this.DialogContainerId;
|
container.id = DIALOG_CONTAINER_ID;
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
},
|
}
|
||||||
|
|
||||||
createDialog: function (Element, props, className) {
|
createDialog(Element, props, className) {
|
||||||
|
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a modal view.
|
||||||
|
*
|
||||||
|
* This can be used to display a react component which is loaded as an asynchronous
|
||||||
|
* webpack component. To do this, set 'loader' as:
|
||||||
|
*
|
||||||
|
* (cb) => {
|
||||||
|
* require(['<module>'], cb);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {Function} loader a function which takes a 'callback' argument,
|
||||||
|
* which it should call with a React component which will be displayed as
|
||||||
|
* the modal view.
|
||||||
|
*
|
||||||
|
* @param {Object} props properties to pass to the displayed
|
||||||
|
* component. (We will also pass an 'onFinished' property.)
|
||||||
|
*
|
||||||
|
* @param {String} className CSS class to apply to the modal wrapper
|
||||||
|
*/
|
||||||
|
createDialogAsync(loader, props, className) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
const modal = {};
|
||||||
|
|
||||||
// never call this via modal.close() from onFinished() otherwise it will loop
|
// never call this from onFinished() otherwise it will loop
|
||||||
|
//
|
||||||
|
// nb explicit function() rather than arrow function, to get `arguments`
|
||||||
var closeDialog = function() {
|
var closeDialog = function() {
|
||||||
if (props && props.onFinished) props.onFinished.apply(null, arguments);
|
if (props && props.onFinished) props.onFinished.apply(null, arguments);
|
||||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
var i = self._modals.indexOf(modal);
|
||||||
|
if (i >= 0) {
|
||||||
|
self._modals.splice(i, 1);
|
||||||
|
}
|
||||||
|
self._reRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// don't attempt to reuse the same AsyncWrapper for different dialogs,
|
||||||
|
// otherwise we'll get confused.
|
||||||
|
const modalCount = this._counter++;
|
||||||
|
|
||||||
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
|
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
|
||||||
// property set here so you can't close the dialog from a button click!
|
// property set here so you can't close the dialog from a button click!
|
||||||
|
modal.elem = (
|
||||||
|
<AsyncWrapper key={modalCount} loader={loader} {...props}
|
||||||
|
onFinished={closeDialog}/>
|
||||||
|
);
|
||||||
|
modal.onFinished = props ? props.onFinished : null;
|
||||||
|
modal.className = className;
|
||||||
|
|
||||||
|
this._modals.unshift(modal);
|
||||||
|
|
||||||
|
this._reRender();
|
||||||
|
return {close: closeDialog};
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAll() {
|
||||||
|
const modals = this._modals;
|
||||||
|
this._modals = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < modals.length; i++) {
|
||||||
|
const m = modals[i];
|
||||||
|
if (m.onFinished) {
|
||||||
|
m.onFinished(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._reRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
_reRender() {
|
||||||
|
if (this._modals.length == 0) {
|
||||||
|
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modal = this._modals[0];
|
||||||
var dialog = (
|
var dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + className}>
|
<div className={"mx_Dialog_wrapper " + modal.className}>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
<Element {...props} onFinished={closeDialog}/>
|
{modal.elem}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
|
<div className="mx_Dialog_background" onClick={ this.closeAll }></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDOM.render(dialog, this.getOrCreateContainer());
|
ReactDOM.render(dialog, this.getOrCreateContainer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {close: closeDialog};
|
export default new ModalManager();
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ var Notifier = {
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
var title;
|
var title;
|
||||||
if (!ev.sender || room.name == ev.sender.name) {
|
if (!ev.sender || room.name == ev.sender.name) {
|
||||||
title = room.name;
|
title = room.name;
|
||||||
// notificationMessageForEvent includes sender,
|
// notificationMessageForEvent includes sender,
|
||||||
// but we already have the sender here
|
// but we already have the sender here
|
||||||
|
@ -73,7 +73,7 @@ var Notifier = {
|
||||||
ev.sender, 40, 40, 'crop'
|
ev.sender, 40, 40, 'crop'
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const notif = plaf.displayNotification(title, msg, avatarUrl);
|
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
|
||||||
|
|
||||||
// if displayNotification returns non-null, the platform supports
|
// if displayNotification returns non-null, the platform supports
|
||||||
// clearing notifications later, so keep track of this.
|
// clearing notifications later, so keep track of this.
|
||||||
|
@ -88,7 +88,7 @@ var Notifier = {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.load();
|
e.load();
|
||||||
e.play();
|
e.play();
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
SelectionState,
|
SelectionState,
|
||||||
Entity,
|
Entity,
|
||||||
} from 'draft-js';
|
} from 'draft-js';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import * as emojione from 'emojione';
|
import * as emojione from 'emojione';
|
||||||
import {stateToHTML} from 'draft-js-export-html';
|
import {stateToHTML} from 'draft-js-export-html';
|
||||||
import {SelectionRange} from "./autocomplete/Autocompleter";
|
import {SelectionRange} from "./autocomplete/Autocompleter";
|
||||||
|
|
|
@ -26,7 +26,7 @@ function tsOfNewestEvent(room) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mostRecentActivityFirst(roomList) {
|
function mostRecentActivityFirst(roomList) {
|
||||||
return roomList.sort(function(a,b) {
|
return roomList.sort(function(a, b) {
|
||||||
return tsOfNewestEvent(b) - tsOfNewestEvent(a);
|
return tsOfNewestEvent(b) - tsOfNewestEvent(a);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,7 @@ function isRuleForRoom(roomId, rule) {
|
||||||
}
|
}
|
||||||
const cond = rule.conditions[0];
|
const cond = rule.conditions[0];
|
||||||
if (
|
if (
|
||||||
cond.kind == 'event_match' &&
|
cond.kind == 'event_match' &&
|
||||||
cond.key == 'room_id' &&
|
cond.key == 'room_id' &&
|
||||||
cond.pattern == roomId
|
cond.pattern == roomId
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ export function getOnlyOtherMember(room, me) {
|
||||||
|
|
||||||
if (joinedMembers.length === 2) {
|
if (joinedMembers.length === 2) {
|
||||||
return joinedMembers.filter(function(m) {
|
return joinedMembers.filter(function(m) {
|
||||||
return m.userId !== me.userId
|
return m.userId !== me.userId;
|
||||||
})[0];
|
})[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
80
src/RtsClient.js
Normal file
80
src/RtsClient.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
|
function checkStatus(response) {
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.text().then((text) => {
|
||||||
|
throw new Error(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJson(response) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeQueryParams(params) {
|
||||||
|
return '?' + Object.keys(params).map((k) => {
|
||||||
|
return k + '=' + encodeURIComponent(params[k]);
|
||||||
|
}).join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = (url, opts) => {
|
||||||
|
if (opts && opts.qs) {
|
||||||
|
url += encodeQueryParams(opts.qs);
|
||||||
|
delete opts.qs;
|
||||||
|
}
|
||||||
|
if (opts && opts.body) {
|
||||||
|
if (!opts.headers) {
|
||||||
|
opts.headers = {};
|
||||||
|
}
|
||||||
|
opts.body = JSON.stringify(opts.body);
|
||||||
|
opts.headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
return fetch(url, opts)
|
||||||
|
.then(checkStatus)
|
||||||
|
.then(parseJson);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class RtsClient {
|
||||||
|
constructor(url) {
|
||||||
|
this._url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTeamsConfig() {
|
||||||
|
return request(this._url + '/teams');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track a referral with the Riot Team Server. This should be called once a referred
|
||||||
|
* user has been successfully registered.
|
||||||
|
* @param {string} referrer the user ID of one who referred the user to Riot.
|
||||||
|
* @param {string} userId the user ID of the user being referred.
|
||||||
|
* @param {string} userEmail the email address linked to `userId`.
|
||||||
|
* @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon
|
||||||
|
* success.
|
||||||
|
*/
|
||||||
|
trackReferral(referrer, userId, userEmail) {
|
||||||
|
return request(this._url + '/register',
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
referrer: referrer,
|
||||||
|
user_id: userId,
|
||||||
|
user_email: userEmail,
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTeam(teamToken) {
|
||||||
|
return request(this._url + '/teamConfiguration',
|
||||||
|
{
|
||||||
|
qs: {
|
||||||
|
team_token: teamToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -292,12 +292,15 @@ const onMessage = function(event) {
|
||||||
event.origin = event.originalEvent.origin;
|
event.origin = event.originalEvent.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check it is from the integrations UI URL (remove trailing spaces)
|
// Check that the integrations UI URL starts with the origin of the event
|
||||||
|
// This means the URL could contain a path (like /develop) and still be used
|
||||||
|
// to validate event origins, which do not specify paths.
|
||||||
|
// (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
|
||||||
|
//
|
||||||
|
// All strings start with the empty string, so for sanity return if the length
|
||||||
|
// of the event origin is 0.
|
||||||
let url = SdkConfig.get().integrations_ui_url;
|
let url = SdkConfig.get().integrations_ui_url;
|
||||||
if (url.endsWith("/")) {
|
if (event.origin.length === 0 || !url.startsWith(event.origin)) {
|
||||||
url = url.substr(0, url.length - 1);
|
|
||||||
}
|
|
||||||
if (url !== event.origin) {
|
|
||||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -368,7 +371,7 @@ const onMessage = function(event) {
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
sendError(event, "Failed to lookup current room.");
|
sendError(event, "Failed to lookup current room.");
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -19,6 +19,8 @@ var DEFAULTS = {
|
||||||
integrations_ui_url: "https://scalar.vector.im/",
|
integrations_ui_url: "https://scalar.vector.im/",
|
||||||
// Base URL to the REST interface of the integrations server
|
// Base URL to the REST interface of the integrations server
|
||||||
integrations_rest_url: "https://scalar.vector.im/api",
|
integrations_rest_url: "https://scalar.vector.im/api",
|
||||||
|
// Where to send bug reports. If not specified, bugs cannot be sent.
|
||||||
|
bug_report_endpoint_url: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
class SdkConfig {
|
class SdkConfig {
|
||||||
|
|
|
@ -191,7 +191,7 @@ class Register extends Signup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (poll_for_success) {
|
if (poll_for_success) {
|
||||||
return q.delay(5000).then(function() {
|
return q.delay(2000).then(function() {
|
||||||
return self._tryRegister(client, authDict, poll_for_success);
|
return self._tryRegister(client, authDict, poll_for_success);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -203,7 +203,17 @@ class Register extends Signup {
|
||||||
} else if (error.errcode == 'M_INVALID_USERNAME') {
|
} else if (error.errcode == 'M_INVALID_USERNAME') {
|
||||||
throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
|
throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
|
||||||
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
||||||
throw new Error(`Registration failed! (${error.httpStatus})`);
|
let msg = null;
|
||||||
|
if (error.message) {
|
||||||
|
msg = error.message;
|
||||||
|
} else if (error.errcode) {
|
||||||
|
msg = error.errcode;
|
||||||
|
}
|
||||||
|
if (msg) {
|
||||||
|
throw new Error(`Registration failed! (${error.httpStatus}) - ${msg}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Registration failed! (${error.httpStatus}) - That's all we know.`);
|
||||||
|
}
|
||||||
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
|
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Server error during registration! (${error.httpStatus})`
|
`Server error during registration! (${error.httpStatus})`
|
||||||
|
|
|
@ -52,7 +52,13 @@ DummyStage.TYPE = "m.login.dummy";
|
||||||
class RecaptchaStage extends Stage {
|
class RecaptchaStage extends Stage {
|
||||||
constructor(matrixClient, signupInstance) {
|
constructor(matrixClient, signupInstance) {
|
||||||
super(RecaptchaStage.TYPE, matrixClient, signupInstance);
|
super(RecaptchaStage.TYPE, matrixClient, signupInstance);
|
||||||
this.defer = q.defer(); // resolved with the captcha response
|
this.authDict = {
|
||||||
|
auth: {
|
||||||
|
type: 'm.login.recaptcha',
|
||||||
|
// we'll add in the response param if we get one from the local user.
|
||||||
|
},
|
||||||
|
poll_for_success: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// called when the recaptcha has been completed.
|
// called when the recaptcha has been completed.
|
||||||
|
@ -60,16 +66,15 @@ class RecaptchaStage extends Stage {
|
||||||
if (!data || !data.response) {
|
if (!data || !data.response) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.defer.resolve({
|
this.authDict.auth.response = data.response;
|
||||||
auth: {
|
|
||||||
type: 'm.login.recaptcha',
|
|
||||||
response: data.response,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
complete() {
|
complete() {
|
||||||
return this.defer.promise;
|
// we return the authDict with no response, telling Signup to keep polling
|
||||||
|
// the server in case the captcha is filled in on another window (e.g. by
|
||||||
|
// following a nextlink from an email signup). If the user completes the
|
||||||
|
// captcha locally, then we return at the next poll.
|
||||||
|
return q(this.authDict);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RecaptchaStage.TYPE = "m.login.recaptcha";
|
RecaptchaStage.TYPE = "m.login.recaptcha";
|
||||||
|
|
|
@ -41,7 +41,7 @@ class Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsage() {
|
getUsage() {
|
||||||
return "Usage: " + this.getCommandWithArgs()
|
return "Usage: " + this.getCommandWithArgs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ var commands = {
|
||||||
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
|
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
Tinter.tint(matches[1], matches[4]);
|
Tinter.tint(matches[1], matches[4]);
|
||||||
var colorScheme = {}
|
var colorScheme = {};
|
||||||
colorScheme.primary_color = matches[1];
|
colorScheme.primary_color = matches[1];
|
||||||
if (matches[4]) {
|
if (matches[4]) {
|
||||||
colorScheme.secondary_color = matches[4];
|
colorScheme.secondary_color = matches[4];
|
||||||
|
@ -288,7 +288,7 @@ var commands = {
|
||||||
// helpful aliases
|
// helpful aliases
|
||||||
var aliases = {
|
var aliases = {
|
||||||
j: "join"
|
j: "join"
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
|
@ -331,9 +331,9 @@ module.exports = {
|
||||||
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
||||||
var cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
var cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
||||||
return commands[cmdKey];
|
return commands[cmdKey];
|
||||||
})
|
});
|
||||||
cmds.push(new Command("me", "<action>", function(){}));
|
cmds.push(new Command("me", "<action>", function() {}));
|
||||||
cmds.push(new Command("markdown", "<on|off>", function(){}));
|
cmds.push(new Command("markdown", "<on|off>", function() {}));
|
||||||
|
|
||||||
return cmds;
|
return cmds;
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ class TabComplete {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ES6 destructuring; ignore first element (the complete match)
|
// ES6 destructuring; ignore first element (the complete match)
|
||||||
var [ , boundaryGroup, partialGroup] = res;
|
var [, boundaryGroup, partialGroup] = res;
|
||||||
|
|
||||||
if (partialGroup.length === 0 && passive) {
|
if (partialGroup.length === 0 && passive) {
|
||||||
return;
|
return;
|
||||||
|
@ -227,8 +227,20 @@ class TabComplete {
|
||||||
|
|
||||||
// pressing any key at all (except tab) restarts the automatic tab-complete timer
|
// pressing any key at all (except tab) restarts the automatic tab-complete timer
|
||||||
if (this.opts.autoEnterTabComplete) {
|
if (this.opts.autoEnterTabComplete) {
|
||||||
|
const cachedText = ev.target.value;
|
||||||
clearTimeout(this.enterTabCompleteTimerId);
|
clearTimeout(this.enterTabCompleteTimerId);
|
||||||
this.enterTabCompleteTimerId = setTimeout(() => {
|
this.enterTabCompleteTimerId = setTimeout(() => {
|
||||||
|
if (this.completing) {
|
||||||
|
// If you highlight text and CTRL+X it, tab-completing will not be reset.
|
||||||
|
// This check makes sure that if something like a cut operation has been
|
||||||
|
// done, that we correctly refresh the tab-complete list. Normal backspace
|
||||||
|
// operations get caught by the stopTabCompleting() section above, but
|
||||||
|
// because the CTRL key is held, this does not execute for CTRL+X.
|
||||||
|
if (cachedText !== this.textArea.value) {
|
||||||
|
this.stopTabCompleting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.completing) {
|
if (!this.completing) {
|
||||||
this.handleTabPress(true, false);
|
this.handleTabPress(true, false);
|
||||||
}
|
}
|
||||||
|
@ -242,7 +254,7 @@ class TabComplete {
|
||||||
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
||||||
|
|
||||||
// tab key has been pressed at this point
|
// tab key has been pressed at this point
|
||||||
this.handleTabPress(false, ev.shiftKey)
|
this.handleTabPress(false, ev.shiftKey);
|
||||||
|
|
||||||
// prevent the default TAB operation (typically focus shifting)
|
// prevent the default TAB operation (typically focus shifting)
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -374,6 +386,6 @@ class TabComplete {
|
||||||
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
|
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = TabComplete;
|
module.exports = TabComplete;
|
||||||
|
|
|
@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
var React = require("react");
|
|
||||||
var sdk = require("./index");
|
var sdk = require("./index");
|
||||||
|
|
||||||
class Entry {
|
class Entry {
|
||||||
|
@ -90,7 +89,7 @@ CommandEntry.fromCommands = function(commandArray) {
|
||||||
return commandArray.map(function(cmd) {
|
return commandArray.map(function(cmd) {
|
||||||
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
|
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
class MemberEntry extends Entry {
|
class MemberEntry extends Entry {
|
||||||
constructor(member) {
|
constructor(member) {
|
||||||
|
@ -119,7 +118,7 @@ MemberEntry.fromMemberList = function(members) {
|
||||||
return members.map(function(m) {
|
return members.map(function(m) {
|
||||||
return new MemberEntry(m);
|
return new MemberEntry(m);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports.Entry = Entry;
|
module.exports.Entry = Entry;
|
||||||
module.exports.MemberEntry = MemberEntry;
|
module.exports.MemberEntry = MemberEntry;
|
||||||
|
|
|
@ -75,7 +75,6 @@ function textForMemberEvent(ev) {
|
||||||
return targetName + " joined the room.";
|
return targetName + " joined the room.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '';
|
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||||
|
@ -203,4 +202,4 @@ module.exports = {
|
||||||
if (!hdlr) return "";
|
if (!hdlr) return "";
|
||||||
return hdlr(ev);
|
return hdlr(ev);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -14,9 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var dis = require("./dispatcher");
|
|
||||||
var sdk = require("./index");
|
|
||||||
|
|
||||||
// FIXME: these vars should be bundled up and attached to
|
// FIXME: these vars should be bundled up and attached to
|
||||||
// module.exports otherwise this will break when included by both
|
// module.exports otherwise this will break when included by both
|
||||||
// react-sdk and apps layered on top.
|
// react-sdk and apps layered on top.
|
||||||
|
@ -42,6 +39,7 @@ var keyHex = [
|
||||||
"#76CFA6", // Vector Green
|
"#76CFA6", // Vector Green
|
||||||
"#EAF5F0", // Vector Light Green
|
"#EAF5F0", // Vector Light Green
|
||||||
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
|
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
|
||||||
|
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
|
||||||
];
|
];
|
||||||
|
|
||||||
// cache of our replacement colours
|
// cache of our replacement colours
|
||||||
|
@ -50,6 +48,7 @@ var colors = [
|
||||||
keyHex[0],
|
keyHex[0],
|
||||||
keyHex[1],
|
keyHex[1],
|
||||||
keyHex[2],
|
keyHex[2],
|
||||||
|
keyHex[3],
|
||||||
];
|
];
|
||||||
|
|
||||||
var cssFixups = [
|
var cssFixups = [
|
||||||
|
@ -150,10 +149,28 @@ function hexToRgb(color) {
|
||||||
|
|
||||||
function rgbToHex(rgb) {
|
function rgbToHex(rgb) {
|
||||||
var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
||||||
return '#' + (0x1000000 + val).toString(16).slice(1)
|
return '#' + (0x1000000 + val).toString(16).slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List of functions to call when the tint changes.
|
||||||
|
const tintables = [];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Register a callback to fire when the tint changes.
|
||||||
|
* This is used to rewrite the tintable SVGs with the new tint.
|
||||||
|
*
|
||||||
|
* It's not possible to unregister a tintable callback. So this can only be
|
||||||
|
* used to register a static callback. If a set of tintables will change
|
||||||
|
* over time then the best bet is to register a single callback for the
|
||||||
|
* entire set.
|
||||||
|
*
|
||||||
|
* @param {Function} tintable Function to call when the tint changes.
|
||||||
|
*/
|
||||||
|
registerTintable : function(tintable) {
|
||||||
|
tintables.push(tintable);
|
||||||
|
},
|
||||||
|
|
||||||
tint: function(primaryColor, secondaryColor, tertiaryColor) {
|
tint: function(primaryColor, secondaryColor, tertiaryColor) {
|
||||||
|
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
|
@ -167,7 +184,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!secondaryColor) {
|
if (!secondaryColor) {
|
||||||
var x = 0.16; // average weighting factor calculated from vector green & light green
|
const x = 0.16; // average weighting factor calculated from vector green & light green
|
||||||
var rgb = hexToRgb(primaryColor);
|
var rgb = hexToRgb(primaryColor);
|
||||||
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
||||||
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
||||||
|
@ -176,7 +193,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tertiaryColor) {
|
if (!tertiaryColor) {
|
||||||
var x = 0.19;
|
const x = 0.19;
|
||||||
var rgb1 = hexToRgb(primaryColor);
|
var rgb1 = hexToRgb(primaryColor);
|
||||||
var rgb2 = hexToRgb(secondaryColor);
|
var rgb2 = hexToRgb(secondaryColor);
|
||||||
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
||||||
|
@ -192,7 +209,9 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
colors = [primaryColor, secondaryColor, tertiaryColor];
|
colors[0] = primaryColor;
|
||||||
|
colors[1] = secondaryColor;
|
||||||
|
colors[2] = tertiaryColor;
|
||||||
|
|
||||||
if (DEBUG) console.log("Tinter.tint");
|
if (DEBUG) console.log("Tinter.tint");
|
||||||
|
|
||||||
|
@ -201,12 +220,22 @@ module.exports = {
|
||||||
|
|
||||||
// tell all the SVGs to go fix themselves up
|
// tell all the SVGs to go fix themselves up
|
||||||
// we don't do this as a dispatch otherwise it will visually lag
|
// we don't do this as a dispatch otherwise it will visually lag
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
tintables.forEach(function(tintable) {
|
||||||
if (TintableSvg.mounts) {
|
tintable();
|
||||||
Object.keys(TintableSvg.mounts).forEach((id) => {
|
});
|
||||||
TintableSvg.mounts[id].tint();
|
},
|
||||||
});
|
|
||||||
|
tintSvgWhite: function(whiteColor) {
|
||||||
|
if (!whiteColor) {
|
||||||
|
whiteColor = colors[3];
|
||||||
}
|
}
|
||||||
|
if (colors[3] === whiteColor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
colors[3] = whiteColor;
|
||||||
|
tintables.forEach(function(tintable) {
|
||||||
|
tintable();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// XXX: we could just move this all into TintableSvg, but as it's so similar
|
// XXX: we could just move this all into TintableSvg, but as it's so similar
|
||||||
|
@ -265,5 +294,5 @@ module.exports = {
|
||||||
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
|
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
|
||||||
}
|
}
|
||||||
if (DEBUG) console.log("applySvgFixups end");
|
if (DEBUG) console.log("applySvgFixups end");
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,16 +30,6 @@ module.exports = {
|
||||||
id: 'rich_text_editor',
|
id: 'rich_text_editor',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'End-to-End Encryption',
|
|
||||||
id: 'e2e_encryption',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Integration Management',
|
|
||||||
id: 'integration_management',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
|
||||||
loadProfileInfo: function() {
|
loadProfileInfo: function() {
|
||||||
|
|
|
@ -76,7 +76,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var startStyles = self.props.startStyles;
|
var startStyles = self.props.startStyles;
|
||||||
if (startStyles.length > 0) {
|
if (startStyles.length > 0) {
|
||||||
var startStyle = startStyles[0]
|
var startStyle = startStyles[0];
|
||||||
newProps.style = startStyle;
|
newProps.style = startStyle;
|
||||||
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ module.exports = React.createClass({
|
||||||
) {
|
) {
|
||||||
var startStyles = this.props.startStyles;
|
var startStyles = this.props.startStyles;
|
||||||
var transitionOpts = this.props.enterTransitionOpts;
|
var transitionOpts = this.props.enterTransitionOpts;
|
||||||
var domNode = ReactDom.findDOMNode(node);
|
const domNode = ReactDom.findDOMNode(node);
|
||||||
// start from startStyle 1: 0 is the one we gave it
|
// start from startStyle 1: 0 is the one we gave it
|
||||||
// to start with, so now we animate 1 etc.
|
// to start with, so now we animate 1 etc.
|
||||||
for (var i = 1; i < startStyles.length; ++i) {
|
for (var i = 1; i < startStyles.length; ++i) {
|
||||||
|
@ -145,7 +145,7 @@ module.exports = React.createClass({
|
||||||
// and the FAQ entry, "Preventing memory leaks when
|
// and the FAQ entry, "Preventing memory leaks when
|
||||||
// creating/destroying large numbers of elements"
|
// creating/destroying large numbers of elements"
|
||||||
// (https://github.com/julianshapiro/velocity/issues/47)
|
// (https://github.com/julianshapiro/velocity/issues/47)
|
||||||
var domNode = ReactDom.findDOMNode(this.nodes[k]);
|
const domNode = ReactDom.findDOMNode(this.nodes[k]);
|
||||||
Velocity.Utilities.removeData(domNode);
|
Velocity.Utilities.removeData(domNode);
|
||||||
}
|
}
|
||||||
this.nodes[k] = node;
|
this.nodes[k] = node;
|
||||||
|
|
|
@ -6,10 +6,12 @@ function bounce( p ) {
|
||||||
var pow2,
|
var pow2,
|
||||||
bounce = 4;
|
bounce = 4;
|
||||||
|
|
||||||
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
|
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
|
||||||
|
// just sets pow2
|
||||||
|
}
|
||||||
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
|
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
Velocity.Easings.easeOutBounce = function(p) {
|
Velocity.Easings.easeOutBounce = function(p) {
|
||||||
return 1 - bounce(1 - p);
|
return 1 - bounce(1 - p);
|
||||||
}
|
};
|
||||||
|
|
|
@ -32,18 +32,25 @@ module.exports = {
|
||||||
return whoIsTyping;
|
return whoIsTyping;
|
||||||
},
|
},
|
||||||
|
|
||||||
whoIsTypingString: function(room) {
|
whoIsTypingString: function(room, limit) {
|
||||||
var whoIsTyping = this.usersTypingApartFromMe(room);
|
const whoIsTyping = this.usersTypingApartFromMe(room);
|
||||||
|
const othersCount = limit === undefined ?
|
||||||
|
0 : Math.max(whoIsTyping.length - limit, 0);
|
||||||
if (whoIsTyping.length == 0) {
|
if (whoIsTyping.length == 0) {
|
||||||
return null;
|
return '';
|
||||||
} else if (whoIsTyping.length == 1) {
|
} else if (whoIsTyping.length == 1) {
|
||||||
return whoIsTyping[0].name + ' is typing';
|
return whoIsTyping[0].name + ' is typing';
|
||||||
|
}
|
||||||
|
const names = whoIsTyping.map(function(m) {
|
||||||
|
return m.name;
|
||||||
|
});
|
||||||
|
if (othersCount) {
|
||||||
|
const other = ' other' + (othersCount > 1 ? 's' : '');
|
||||||
|
return names.slice(0, limit).join(', ') + ' and ' +
|
||||||
|
othersCount + other + ' are typing';
|
||||||
} else {
|
} else {
|
||||||
var names = whoIsTyping.map(function(m) {
|
const lastPerson = names.pop();
|
||||||
return m.name;
|
|
||||||
});
|
|
||||||
var lastPerson = names.shift();
|
|
||||||
return names.join(', ') + ' and ' + lastPerson + ' are typing';
|
return names.join(', ') + ' and ' + lastPerson + ' are typing';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -83,7 +83,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var verificationStatus = (<b>NOT verified</b>);
|
var verificationStatus = (<b>NOT verified</b>);
|
||||||
if (device.isBlocked()) {
|
if (device.isBlocked()) {
|
||||||
verificationStatus = (<b>Blocked</b>);
|
verificationStatus = (<b>Blacklisted</b>);
|
||||||
} else if (device.isVerified()) {
|
} else if (device.isVerified()) {
|
||||||
verificationStatus = "verified";
|
verificationStatus = "verified";
|
||||||
}
|
}
|
175
src/async-components/views/dialogs/ExportE2eKeysDialog.js
Normal file
175
src/async-components/views/dialogs/ExportE2eKeysDialog.js
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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 FileSaver from 'file-saver';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
|
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
const PHASE_EDIT = 1;
|
||||||
|
const PHASE_EXPORTING = 2;
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'ExportE2eKeysDialog',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
phase: PHASE_EDIT,
|
||||||
|
errStr: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onPassphraseFormSubmit: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const passphrase = this.refs.passphrase1.value;
|
||||||
|
if (passphrase !== this.refs.passphrase2.value) {
|
||||||
|
this.setState({errStr: 'Passphrases must match'});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!passphrase) {
|
||||||
|
this.setState({errStr: 'Passphrase must not be empty'});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._startExport(passphrase);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_startExport: function(passphrase) {
|
||||||
|
// extra Promise.resolve() to turn synchronous exceptions into
|
||||||
|
// asynchronous ones.
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
return this.props.matrixClient.exportRoomKeys();
|
||||||
|
}).then((k) => {
|
||||||
|
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||||
|
JSON.stringify(k), passphrase,
|
||||||
|
);
|
||||||
|
}).then((f) => {
|
||||||
|
const blob = new Blob([f], {
|
||||||
|
type: 'text/plain;charset=us-ascii',
|
||||||
|
});
|
||||||
|
FileSaver.saveAs(blob, 'riot-keys.txt');
|
||||||
|
this.props.onFinished(true);
|
||||||
|
}).catch((e) => {
|
||||||
|
if (this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errStr: e.message,
|
||||||
|
phase: PHASE_EDIT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
errStr: null,
|
||||||
|
phase: PHASE_EXPORTING,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCancelClick: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.props.onFinished(false);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
|
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_exportE2eKeysDialog'
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title="Export room keys"
|
||||||
|
>
|
||||||
|
<form onSubmit={this._onPassphraseFormSubmit}>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<p>
|
||||||
|
This process allows you to export the keys for messages
|
||||||
|
you have received in encrypted rooms to a local file. You
|
||||||
|
will then be able to import the file into another Matrix
|
||||||
|
client in the future, so that client will also be able to
|
||||||
|
decrypt these messages.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The exported file will allow anyone who can read it to decrypt
|
||||||
|
any encrypted messages that you can see, so you should be
|
||||||
|
careful to keep it secure. To help with this, you should enter
|
||||||
|
a passphrase below, which will be used to encrypt the exported
|
||||||
|
data. It will only be possible to import the data by using the
|
||||||
|
same passphrase.
|
||||||
|
</p>
|
||||||
|
<div className='error'>
|
||||||
|
{this.state.errStr}
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputTable'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
|
<label htmlFor='passphrase1'>
|
||||||
|
Enter passphrase
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
<input ref='passphrase1' id='passphrase1'
|
||||||
|
autoFocus={true} size='64' type='password'
|
||||||
|
disabled={disableForm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
|
<label htmlFor='passphrase2'>
|
||||||
|
Confirm passphrase
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
<input ref='passphrase2' id='passphrase2'
|
||||||
|
size='64' type='password'
|
||||||
|
disabled={disableForm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx_Dialog_buttons'>
|
||||||
|
<input className='mx_Dialog_primary' type='submit' value='Export'
|
||||||
|
disabled={disableForm}
|
||||||
|
/>
|
||||||
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
174
src/async-components/views/dialogs/ImportE2eKeysDialog.js
Normal file
174
src/async-components/views/dialogs/ImportE2eKeysDialog.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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 * as Matrix from 'matrix-js-sdk';
|
||||||
|
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
function readFileAsArrayBuffer(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
resolve(e.target.result);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const PHASE_EDIT = 1;
|
||||||
|
const PHASE_IMPORTING = 2;
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'ImportE2eKeysDialog',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
enableSubmit: false,
|
||||||
|
phase: PHASE_EDIT,
|
||||||
|
errStr: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onFormChange: function(ev) {
|
||||||
|
const files = this.refs.file.files || [];
|
||||||
|
this.setState({
|
||||||
|
enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onFormSubmit: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this._startImport(this.refs.file.files[0], this.refs.passphrase.value);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_startImport: function(file, passphrase) {
|
||||||
|
this.setState({
|
||||||
|
errStr: null,
|
||||||
|
phase: PHASE_IMPORTING,
|
||||||
|
});
|
||||||
|
|
||||||
|
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
||||||
|
return MegolmExportEncryption.decryptMegolmKeyFile(
|
||||||
|
arrayBuffer, passphrase,
|
||||||
|
);
|
||||||
|
}).then((keys) => {
|
||||||
|
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
|
||||||
|
}).then(() => {
|
||||||
|
// TODO: it would probably be nice to give some feedback about what we've imported here.
|
||||||
|
this.props.onFinished(true);
|
||||||
|
}).catch((e) => {
|
||||||
|
if (this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errStr: e.message,
|
||||||
|
phase: PHASE_EDIT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCancelClick: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.props.onFinished(false);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
|
const disableForm = (this.state.phase !== PHASE_EDIT);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_importE2eKeysDialog'
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title="Import room keys"
|
||||||
|
>
|
||||||
|
<form onSubmit={this._onFormSubmit}>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<p>
|
||||||
|
This process allows you to import encryption keys
|
||||||
|
that you had previously exported from another Matrix
|
||||||
|
client. You will then be able to decrypt any
|
||||||
|
messages that the other client could decrypt.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The export file will be protected with a passphrase.
|
||||||
|
You should enter the passphrase here, to decrypt the
|
||||||
|
file.
|
||||||
|
</p>
|
||||||
|
<div className='error'>
|
||||||
|
{this.state.errStr}
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputTable'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
|
<label htmlFor='importFile'>
|
||||||
|
File to import
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
<input ref='file' id='importFile' type='file'
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={this._onFormChange}
|
||||||
|
disabled={disableForm} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
|
<label htmlFor='passphrase'>
|
||||||
|
Enter passphrase
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
<input ref='passphrase' id='passphrase'
|
||||||
|
size='64' type='password'
|
||||||
|
onChange={this._onFormChange}
|
||||||
|
disabled={disableForm}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx_Dialog_buttons'>
|
||||||
|
<input className='mx_Dialog_primary' type='submit' value='Import'
|
||||||
|
disabled={!this.state.enableSubmit || disableForm}
|
||||||
|
/>
|
||||||
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -83,7 +83,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
static getInstance(): CommandProvider {
|
static getInstance(): CommandProvider {
|
||||||
if (instance == null)
|
if (instance == null)
|
||||||
instance = new CommandProvider();
|
{instance = new CommandProvider();}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
if (instance == null)
|
if (instance == null)
|
||||||
instance = new EmojiProvider();
|
{instance = new EmojiProvider();}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,18 +71,16 @@ import views$create_room$Presets from './components/views/create_room/Presets';
|
||||||
views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets);
|
views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets);
|
||||||
import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
|
import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
|
||||||
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
|
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
|
||||||
|
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
|
||||||
|
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
|
||||||
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
||||||
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
||||||
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
||||||
views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog);
|
views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog);
|
||||||
import views$dialogs$EncryptedEventDialog from './components/views/dialogs/EncryptedEventDialog';
|
|
||||||
views$dialogs$EncryptedEventDialog && (module.exports.components['views.dialogs.EncryptedEventDialog'] = views$dialogs$EncryptedEventDialog);
|
|
||||||
import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
|
import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
|
||||||
views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
|
views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
|
||||||
import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';
|
import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';
|
||||||
views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog);
|
views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog);
|
||||||
import views$dialogs$LogoutPrompt from './components/views/dialogs/LogoutPrompt';
|
|
||||||
views$dialogs$LogoutPrompt && (module.exports.components['views.dialogs.LogoutPrompt'] = views$dialogs$LogoutPrompt);
|
|
||||||
import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog';
|
import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog';
|
||||||
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
|
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
|
||||||
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
|
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
|
||||||
|
@ -91,6 +89,8 @@ import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDi
|
||||||
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
|
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
|
||||||
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
|
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
|
||||||
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
|
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
|
||||||
|
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
|
||||||
|
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
|
||||||
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
||||||
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
|
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
|
||||||
import views$elements$AddressTile from './components/views/elements/AddressTile';
|
import views$elements$AddressTile from './components/views/elements/AddressTile';
|
||||||
|
|
|
@ -47,7 +47,7 @@ module.exports = {
|
||||||
return container;
|
return container;
|
||||||
},
|
},
|
||||||
|
|
||||||
createMenu: function (Element, props) {
|
createMenu: function(Element, props) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var closeMenu = function() {
|
var closeMenu = function() {
|
||||||
|
@ -67,7 +67,7 @@ module.exports = {
|
||||||
chevronOffset.top = props.chevronOffset;
|
chevronOffset.top = props.chevronOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// To overide the deafult chevron colour, if it's been set
|
// To override the default chevron colour, if it's been set
|
||||||
var chevronCSS = "";
|
var chevronCSS = "";
|
||||||
if (props.menuColour) {
|
if (props.menuColour) {
|
||||||
chevronCSS = `
|
chevronCSS = `
|
||||||
|
@ -78,15 +78,15 @@ module.exports = {
|
||||||
.mx_ContextualMenu_chevron_right:after {
|
.mx_ContextualMenu_chevron_right:after {
|
||||||
border-left-color: ${props.menuColour};
|
border-left-color: ${props.menuColour};
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chevron = null;
|
var chevron = null;
|
||||||
if (props.left) {
|
if (props.left) {
|
||||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>
|
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
|
||||||
position.left = props.left;
|
position.left = props.left;
|
||||||
} else {
|
} else {
|
||||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>
|
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
|
||||||
position.right = props.right;
|
position.right = props.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
deferred.then(function (resp) {
|
deferred.then(function(resp) {
|
||||||
self.setState({
|
self.setState({
|
||||||
phase: self.phases.CREATED,
|
phase: self.phases.CREATED,
|
||||||
});
|
});
|
||||||
|
@ -210,7 +210,7 @@ module.exports = React.createClass({
|
||||||
onAliasChanged: function(alias) {
|
onAliasChanged: function(alias) {
|
||||||
this.setState({
|
this.setState({
|
||||||
alias: alias
|
alias: alias
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onEncryptChanged: function(ev) {
|
onEncryptChanged: function(ev) {
|
||||||
|
|
|
@ -35,7 +35,7 @@ var FilePanel = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
timelineSet: null,
|
timelineSet: null,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import KeyCode from '../../KeyCode';
|
||||||
import Notifier from '../../Notifier';
|
import Notifier from '../../Notifier';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
|
import dis from '../../dispatcher';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||||
|
@ -161,8 +162,8 @@ export default React.createClass({
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapse_rhs}
|
||||||
ConferenceHandler={this.props.ConferenceHandler}
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
scrollStateMap={this._scrollStateMap}
|
scrollStateMap={this._scrollStateMap}
|
||||||
/>
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />
|
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.UserSettings:
|
case PageTypes.UserSettings:
|
||||||
|
@ -171,24 +172,25 @@ export default React.createClass({
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapse_rhs}
|
||||||
enableLabs={this.props.config.enableLabs}
|
enableLabs={this.props.config.enableLabs}
|
||||||
/>
|
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
|
/>;
|
||||||
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.CreateRoom:
|
case PageTypes.CreateRoom:
|
||||||
page_element = <CreateRoom
|
page_element = <CreateRoom
|
||||||
onRoomCreated={this.props.onRoomCreated}
|
onRoomCreated={this.props.onRoomCreated}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapse_rhs}
|
||||||
/>
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.RoomDirectory:
|
case PageTypes.RoomDirectory:
|
||||||
page_element = <RoomDirectory
|
page_element = <RoomDirectory
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapse_rhs}
|
||||||
config={this.props.config.roomDirectory}
|
config={this.props.config.roomDirectory}
|
||||||
/>
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.HomePage:
|
case PageTypes.HomePage:
|
||||||
|
@ -201,7 +203,7 @@ export default React.createClass({
|
||||||
|
|
||||||
case PageTypes.UserView:
|
case PageTypes.UserView:
|
||||||
page_element = null; // deliberately null for now
|
page_element = null; // deliberately null for now
|
||||||
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />
|
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,15 +66,28 @@ module.exports = React.createClass({
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
childContextTypes: {
|
||||||
|
appConfig: React.PropTypes.object,
|
||||||
|
},
|
||||||
|
|
||||||
AuxPanel: {
|
AuxPanel: {
|
||||||
RoomSettings: "room_settings",
|
RoomSettings: "room_settings",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getChildContext: function() {
|
||||||
|
return {
|
||||||
|
appConfig: this.props.config,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
var s = {
|
var s = {
|
||||||
loading: true,
|
loading: true,
|
||||||
screen: undefined,
|
screen: undefined,
|
||||||
|
|
||||||
|
// What the LoggedInView would be showing if visible
|
||||||
|
page_type: null,
|
||||||
|
|
||||||
// If we are viewing a room by alias, this contains the alias
|
// If we are viewing a room by alias, this contains the alias
|
||||||
currentRoomAlias: null,
|
currentRoomAlias: null,
|
||||||
|
|
||||||
|
@ -235,8 +248,6 @@ module.exports = React.createClass({
|
||||||
setStateForNewScreen: function(state) {
|
setStateForNewScreen: function(state) {
|
||||||
const newState = {
|
const newState = {
|
||||||
screen: undefined,
|
screen: undefined,
|
||||||
currentRoomAlias: null,
|
|
||||||
currentRoomId: null,
|
|
||||||
viewUserId: null,
|
viewUserId: null,
|
||||||
logged_in: false,
|
logged_in: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
|
@ -449,6 +460,9 @@ module.exports = React.createClass({
|
||||||
middleOpacity: payload.middleOpacity,
|
middleOpacity: payload.middleOpacity,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'set_theme':
|
||||||
|
this._onSetTheme(payload.value);
|
||||||
|
break;
|
||||||
case 'on_logged_in':
|
case 'on_logged_in':
|
||||||
this._onLoggedIn();
|
this._onLoggedIn();
|
||||||
break;
|
break;
|
||||||
|
@ -579,6 +593,50 @@ module.exports = React.createClass({
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever someone changes the theme
|
||||||
|
*/
|
||||||
|
_onSetTheme: function(theme) {
|
||||||
|
if (!theme) {
|
||||||
|
theme = 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for the stylesheet elements.
|
||||||
|
// styleElements is a map from style name to HTMLLinkElement.
|
||||||
|
var styleElements = Object.create(null);
|
||||||
|
var i, a;
|
||||||
|
for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
||||||
|
var href = a.getAttribute("href");
|
||||||
|
// shouldn't we be using the 'title' tag rather than the href?
|
||||||
|
var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
|
||||||
|
if (match) {
|
||||||
|
styleElements[match[1]] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(theme in styleElements)) {
|
||||||
|
throw new Error("Unknown theme " + theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable all of them first, then enable the one we want. Chrome only
|
||||||
|
// bothers to do an update on a true->false transition, so this ensures
|
||||||
|
// that we get exactly one update, at the right time.
|
||||||
|
|
||||||
|
Object.values(styleElements).forEach((a) => {
|
||||||
|
a.disabled = true;
|
||||||
|
});
|
||||||
|
styleElements[theme].disabled = false;
|
||||||
|
|
||||||
|
if (theme === 'dark') {
|
||||||
|
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
||||||
|
// XXX: obviously this shouldn't be hardcoded here.
|
||||||
|
Tinter.tintSvgWhite('#2d2d2d');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Tinter.tintSvgWhite('#ffffff');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a new logged in session has started
|
* Called when a new logged in session has started
|
||||||
*/
|
*/
|
||||||
|
@ -601,6 +659,9 @@ module.exports = React.createClass({
|
||||||
ready: false,
|
ready: false,
|
||||||
collapse_lhs: false,
|
collapse_lhs: false,
|
||||||
collapse_rhs: false,
|
collapse_rhs: false,
|
||||||
|
currentRoomAlias: null,
|
||||||
|
currentRoomId: null,
|
||||||
|
page_type: PageTypes.RoomDirectory,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -687,6 +748,16 @@ module.exports = React.createClass({
|
||||||
action: 'logout'
|
action: 'logout'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
cli.on("accountData", function(ev) {
|
||||||
|
if (ev.getType() === 'im.vector.web.settings') {
|
||||||
|
if (ev.getContent() && ev.getContent().theme) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'set_theme',
|
||||||
|
value: ev.getContent().theme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onFocus: function(ev) {
|
onFocus: function(ev) {
|
||||||
|
@ -983,7 +1054,7 @@ module.exports = React.createClass({
|
||||||
{...this.props}
|
{...this.props}
|
||||||
{...this.state}
|
{...this.state}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
} else if (this.state.logged_in) {
|
} else if (this.state.logged_in) {
|
||||||
// we think we are logged in, but are still waiting for the /sync to complete
|
// we think we are logged in, but are still waiting for the /sync to complete
|
||||||
var Spinner = sdk.getComponent('elements.Spinner');
|
var Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
@ -1002,11 +1073,13 @@ module.exports = React.createClass({
|
||||||
sessionId={this.state.register_session_id}
|
sessionId={this.state.register_session_id}
|
||||||
idSid={this.state.register_id_sid}
|
idSid={this.state.register_id_sid}
|
||||||
email={this.props.startingFragmentQueryParams.email}
|
email={this.props.startingFragmentQueryParams.email}
|
||||||
|
referrer={this.props.startingFragmentQueryParams.referrer}
|
||||||
username={this.state.upgradeUsername}
|
username={this.state.upgradeUsername}
|
||||||
guestAccessToken={this.state.guestAccessToken}
|
guestAccessToken={this.state.guestAccessToken}
|
||||||
defaultHsUrl={this.getDefaultHsUrl()}
|
defaultHsUrl={this.getDefaultHsUrl()}
|
||||||
defaultIsUrl={this.getDefaultIsUrl()}
|
defaultIsUrl={this.getDefaultIsUrl()}
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
|
teamServerConfig={this.props.config.teamServerConfig}
|
||||||
customHsUrl={this.getCurrentHsUrl()}
|
customHsUrl={this.getCurrentHsUrl()}
|
||||||
customIsUrl={this.getCurrentIsUrl()}
|
customIsUrl={this.getCurrentIsUrl()}
|
||||||
registrationUrl={this.props.registrationUrl}
|
registrationUrl={this.props.registrationUrl}
|
||||||
|
@ -1025,6 +1098,7 @@ module.exports = React.createClass({
|
||||||
customHsUrl={this.getCurrentHsUrl()}
|
customHsUrl={this.getCurrentHsUrl()}
|
||||||
customIsUrl={this.getCurrentIsUrl()}
|
customIsUrl={this.getCurrentIsUrl()}
|
||||||
onComplete={this.onLoginClick}
|
onComplete={this.onLoginClick}
|
||||||
|
onRegisterClick={this.onRegisterClick}
|
||||||
onLoginClick={this.onLoginClick} />
|
onLoginClick={this.onLoginClick} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,7 +19,9 @@ var ReactDOM = require("react-dom");
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
|
|
||||||
var MatrixClientPeg = require('../../MatrixClientPeg')
|
var MatrixClientPeg = require('../../MatrixClientPeg');
|
||||||
|
|
||||||
|
const MILLIS_IN_DAY = 86400000;
|
||||||
|
|
||||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||||
*/
|
*/
|
||||||
|
@ -229,6 +231,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_getEventTiles: function() {
|
_getEventTiles: function() {
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||||
|
|
||||||
this.eventNodes = {};
|
this.eventNodes = {};
|
||||||
|
@ -278,8 +281,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var isMembershipChange = (e) =>
|
var isMembershipChange = (e) =>
|
||||||
e.getType() === 'm.room.member'
|
e.getType() === 'm.room.member'
|
||||||
&& ['join', 'leave'].indexOf(e.event.content.membership) !== -1
|
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
|
||||||
&& (!e.event.prev_content || e.event.content.membership !== e.event.prev_content.membership);
|
|
||||||
|
|
||||||
for (i = 0; i < this.props.events.length; i++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
var mxEv = this.props.events[i];
|
var mxEv = this.props.events[i];
|
||||||
|
@ -292,37 +294,63 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var last = (i == lastShownEventIndex);
|
var last = (i == lastShownEventIndex);
|
||||||
|
|
||||||
// Wrap consecutive member events in a ListSummary
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
if (isMembershipChange(mxEv)) {
|
if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) {
|
||||||
let summarisedEvents = [mxEv];
|
let ts1 = mxEv.getTs();
|
||||||
i++;
|
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||||
for (;i < this.props.events.length; i++) {
|
// member events. This will prevent it from being re-created unnecessarily, and
|
||||||
let collapsedMxEv = this.props.events[i];
|
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
|
||||||
|
// method on MELS can be used to prevent unnecessary renderings.
|
||||||
|
//
|
||||||
|
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
|
||||||
|
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
|
||||||
|
// membership event, which will not change during forward pagination.
|
||||||
|
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||||
|
|
||||||
if (!isMembershipChange(collapsedMxEv)) {
|
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||||
i--;
|
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1}/></li>;
|
||||||
|
ret.push(dateSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
let summarisedEvents = [mxEv];
|
||||||
|
for (;i + 1 < this.props.events.length; i++) {
|
||||||
|
let collapsedMxEv = this.props.events[i + 1];
|
||||||
|
|
||||||
|
// Ignore redacted member events
|
||||||
|
if (!EventTile.haveTileForEvent(collapsedMxEv)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMembershipChange(collapsedMxEv) ||
|
||||||
|
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
summarisedEvents.push(collapsedMxEv);
|
summarisedEvents.push(collapsedMxEv);
|
||||||
}
|
}
|
||||||
// At this point, i = this.props.events.length OR i = the index of the last
|
// At this point, i = the index of the last event in the summary sequence
|
||||||
// MembershipChange in a sequence of MembershipChanges
|
|
||||||
|
|
||||||
let eventTiles = summarisedEvents.map(
|
let eventTiles = summarisedEvents.map(
|
||||||
(e) => {
|
(e) => {
|
||||||
let ret = this._getTilesForEvent(prevEvent, e);
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
|
// of MemberEventListSummary, render each member event as if the previous
|
||||||
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
|
// timestamp of the current event, and no DateSeperator is inserted.
|
||||||
|
let ret = this._getTilesForEvent(e, e);
|
||||||
prevEvent = e;
|
prevEvent = e;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
).reduce((a,b) => a.concat(b));
|
).reduce((a, b) => a.concat(b));
|
||||||
|
|
||||||
if (eventTiles.length === 0) {
|
if (eventTiles.length === 0) {
|
||||||
eventTiles = null;
|
eventTiles = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.push(
|
ret.push(
|
||||||
<MemberEventListSummary key={mxEv.getId()} events={summarisedEvents}>
|
<MemberEventListSummary
|
||||||
{eventTiles}
|
key={key}
|
||||||
|
events={summarisedEvents}
|
||||||
|
data-scroll-token={eventId}>
|
||||||
|
{eventTiles}
|
||||||
</MemberEventListSummary>
|
</MemberEventListSummary>
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
@ -405,12 +433,14 @@ module.exports = React.createClass({
|
||||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||||
// as 'today' for the date separators.
|
// as 'today' for the date separators.
|
||||||
var ts1 = mxEv.getTs();
|
var ts1 = mxEv.getTs();
|
||||||
|
var eventDate = mxEv.getDate();
|
||||||
if (mxEv.status) {
|
if (mxEv.status) {
|
||||||
ts1 = new Date();
|
eventDate = new Date();
|
||||||
|
ts1 = eventDate.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
// do we need a date separator since the last event?
|
// do we need a date separator since the last event?
|
||||||
if (this._wantsDateSeparator(prevEvent, ts1)) {
|
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
continuation = false;
|
continuation = false;
|
||||||
|
@ -447,38 +477,48 @@ module.exports = React.createClass({
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
|
||||||
_wantsDateSeparator: function(prevEvent, nextEventTs) {
|
_wantsDateSeparator: function(prevEvent, nextEventDate) {
|
||||||
if (prevEvent == null) {
|
if (prevEvent == null) {
|
||||||
// first event in the panel: depends if we could back-paginate from
|
// first event in the panel: depends if we could back-paginate from
|
||||||
// here.
|
// here.
|
||||||
return !this.props.suppressFirstDateSeparator;
|
return !this.props.suppressFirstDateSeparator;
|
||||||
}
|
}
|
||||||
|
// Return early for events that are > 24h apart
|
||||||
return (new Date(prevEvent.getTs()).toDateString()
|
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||||
!== new Date(nextEventTs).toDateString());
|
return true;
|
||||||
},
|
|
||||||
|
|
||||||
// get a list of the userids whose read receipts should
|
|
||||||
// be shown next to this event
|
|
||||||
_getReadReceiptsForEvent: function(event) {
|
|
||||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
|
||||||
|
|
||||||
// get list of read receipts, sorted most recent first
|
|
||||||
var room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
|
||||||
if (!room) {
|
|
||||||
// huh.
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return room.getReceiptsForEvent(event).filter(function(r) {
|
// Compare weekdays
|
||||||
return r.type === "m.read" && r.userId != myUserId;
|
return prevEvent.getDate().getDay() !== nextEventDate.getDay();
|
||||||
}).sort(function(r1, r2) {
|
},
|
||||||
return r2.data.ts - r1.data.ts;
|
|
||||||
}).map(function(r) {
|
// get a list of read receipts that should be shown next to this event
|
||||||
return room.getMember(r.userId);
|
// Receipts are objects which have a 'roomMember' and 'ts'.
|
||||||
}).filter(function(m) {
|
_getReadReceiptsForEvent: function(event) {
|
||||||
// check that the user is a known room member
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
return m;
|
|
||||||
|
// get list of read receipts, sorted most recent first
|
||||||
|
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
|
if (!room) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let receipts = [];
|
||||||
|
room.getReceiptsForEvent(event).forEach((r) => {
|
||||||
|
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
||||||
|
return; // ignore non-read receipts and receipts from self.
|
||||||
|
}
|
||||||
|
let member = room.getMember(r.userId);
|
||||||
|
if (!member) {
|
||||||
|
return; // ignore unknown user IDs
|
||||||
|
}
|
||||||
|
receipts.push({
|
||||||
|
roomMember: member,
|
||||||
|
ts: r.data ? r.data.ts : 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return receipts.sort((r1, r2) => {
|
||||||
|
return r2.ts - r1.ts;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -564,6 +604,7 @@ module.exports = React.createClass({
|
||||||
onScroll={ this.props.onScroll }
|
onScroll={ this.props.onScroll }
|
||||||
onResize={ this.onResize }
|
onResize={ this.onResize }
|
||||||
onFillRequest={ this.props.onFillRequest }
|
onFillRequest={ this.props.onFillRequest }
|
||||||
|
onUnfillRequest={ this.props.onUnfillRequest }
|
||||||
style={ style }
|
style={ style }
|
||||||
stickyBottom={ this.props.stickyBottom }>
|
stickyBottom={ this.props.stickyBottom }>
|
||||||
{topSpinner}
|
{topSpinner}
|
||||||
|
|
|
@ -19,6 +19,12 @@ var sdk = require('../../index');
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
var WhoIsTyping = require("../../WhoIsTyping");
|
var WhoIsTyping = require("../../WhoIsTyping");
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
|
const MemberAvatar = require("../views/avatars/MemberAvatar");
|
||||||
|
|
||||||
|
const HIDE_DEBOUNCE_MS = 10000;
|
||||||
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
|
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomStatusBar',
|
displayName: 'RoomStatusBar',
|
||||||
|
@ -45,6 +51,10 @@ module.exports = React.createClass({
|
||||||
// more interesting)
|
// more interesting)
|
||||||
hasActiveCall: React.PropTypes.bool,
|
hasActiveCall: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// Number of names to display in typing indication. E.g. set to 3, will
|
||||||
|
// result in "X, Y, Z and 100 others are typing."
|
||||||
|
whoIsTypingLimit: React.PropTypes.number,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'resend all' button in the
|
// callback for when the user clicks on the 'resend all' button in the
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onResendAllClick: React.PropTypes.func,
|
onResendAllClick: React.PropTypes.func,
|
||||||
|
@ -60,12 +70,28 @@ module.exports = React.createClass({
|
||||||
// status bar. This is used to trigger a re-layout in the parent
|
// status bar. This is used to trigger a re-layout in the parent
|
||||||
// component.
|
// component.
|
||||||
onResize: React.PropTypes.func,
|
onResize: React.PropTypes.func,
|
||||||
|
|
||||||
|
// callback for when the status bar can be hidden from view, as it is
|
||||||
|
// not displaying anything
|
||||||
|
onHidden: React.PropTypes.func,
|
||||||
|
// callback for when the status bar is displaying something and should
|
||||||
|
// be visible
|
||||||
|
onVisible: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
whoIsTypingLimit: 2,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: MatrixClientPeg.get().getSyncState(),
|
||||||
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
|
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
||||||
|
this.props.room,
|
||||||
|
this.props.whoIsTypingLimit
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -78,6 +104,18 @@ module.exports = React.createClass({
|
||||||
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
|
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const size = this._getSize(this.state, this.props);
|
||||||
|
if (size > 0) {
|
||||||
|
this.props.onVisible();
|
||||||
|
} else {
|
||||||
|
if (this.hideDebouncer) {
|
||||||
|
clearTimeout(this.hideDebouncer);
|
||||||
|
}
|
||||||
|
this.hideDebouncer = setTimeout(() => {
|
||||||
|
this.props.onHidden();
|
||||||
|
}, HIDE_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -100,39 +138,35 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onRoomMemberTyping: function(ev, member) {
|
onRoomMemberTyping: function(ev, member) {
|
||||||
this.setState({
|
this.setState({
|
||||||
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
|
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
||||||
|
this.props.room,
|
||||||
|
this.props.whoIsTypingLimit
|
||||||
|
),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// We don't need the actual height - just whether it is likely to have
|
||||||
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
|
// indicate other sizes.
|
||||||
|
_getSize: function(state, props) {
|
||||||
|
if (state.syncState === "ERROR" ||
|
||||||
|
state.whoisTypingString ||
|
||||||
|
props.numUnreadMessages ||
|
||||||
|
!props.atEndOfLiveTimeline ||
|
||||||
|
props.hasActiveCall) {
|
||||||
|
return STATUS_BAR_EXPANDED;
|
||||||
|
} else if (props.tabCompleteEntries) {
|
||||||
|
return STATUS_BAR_HIDDEN;
|
||||||
|
} else if (props.hasUnsentMessages) {
|
||||||
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
|
}
|
||||||
|
return STATUS_BAR_HIDDEN;
|
||||||
|
},
|
||||||
|
|
||||||
// determine if we need to call onResize
|
// determine if we need to call onResize
|
||||||
_checkForResize: function(prevProps, prevState) {
|
_checkForResize: function(prevProps, prevState) {
|
||||||
// figure out the old height and the new height of the status bar. We
|
// figure out the old height and the new height of the status bar.
|
||||||
// don't need the actual height - just whether it is likely to have
|
return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state);
|
||||||
// changed - so we use '0' to indicate normal size, and other values to
|
|
||||||
// indicate other sizes.
|
|
||||||
var oldSize, newSize;
|
|
||||||
|
|
||||||
if (prevState.syncState === "ERROR") {
|
|
||||||
oldSize = 1;
|
|
||||||
} else if (prevProps.tabCompleteEntries) {
|
|
||||||
oldSize = 0;
|
|
||||||
} else if (prevProps.hasUnsentMessages) {
|
|
||||||
oldSize = 2;
|
|
||||||
} else {
|
|
||||||
oldSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.syncState === "ERROR") {
|
|
||||||
newSize = 1;
|
|
||||||
} else if (this.props.tabCompleteEntries) {
|
|
||||||
newSize = 0;
|
|
||||||
} else if (this.props.hasUnsentMessages) {
|
|
||||||
newSize = 2;
|
|
||||||
} else {
|
|
||||||
newSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSize != oldSize;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// return suitable content for the image on the left of the status bar.
|
// return suitable content for the image on the left of the status bar.
|
||||||
|
@ -173,10 +207,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (wantPlaceholder) {
|
if (wantPlaceholder) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_placeholderIndicator">
|
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
||||||
<span>.</span>
|
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)}
|
||||||
<span>.</span>
|
|
||||||
<span>.</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -184,6 +216,36 @@ module.exports = React.createClass({
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderTypingIndicatorAvatars: function(limit) {
|
||||||
|
let users = WhoIsTyping.usersTypingApartFromMe(this.props.room);
|
||||||
|
|
||||||
|
let othersCount = Math.max(users.length - limit, 0);
|
||||||
|
users = users.slice(0, limit);
|
||||||
|
|
||||||
|
let avatars = users.map((u, index) => {
|
||||||
|
let showInitial = othersCount === 0 && index === users.length - 1;
|
||||||
|
return (
|
||||||
|
<MemberAvatar
|
||||||
|
key={u.userId}
|
||||||
|
member={u}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
resizeMethod="crop"
|
||||||
|
defaultToInitialLetter={showInitial}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (othersCount > 0) {
|
||||||
|
avatars.push(
|
||||||
|
<span className="mx_RoomStatusBar_typingIndicatorRemaining">
|
||||||
|
+{othersCount}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatars;
|
||||||
|
},
|
||||||
|
|
||||||
// return suitable content for the main (text) part of the status bar.
|
// return suitable content for the main (text) part of the status bar.
|
||||||
_getContent: function() {
|
_getContent: function() {
|
||||||
|
|
|
@ -48,7 +48,7 @@ if (DEBUG) {
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
var debuglog = console.log.bind(console);
|
var debuglog = console.log.bind(console);
|
||||||
} else {
|
} else {
|
||||||
var debuglog = function () {};
|
var debuglog = function() {};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -146,7 +146,9 @@ module.exports = React.createClass({
|
||||||
showTopUnreadMessagesBar: false,
|
showTopUnreadMessagesBar: false,
|
||||||
|
|
||||||
auxPanelMaxHeight: undefined,
|
auxPanelMaxHeight: undefined,
|
||||||
}
|
|
||||||
|
statusBarVisible: false,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -674,8 +676,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchResultsFillRequest: function(backwards) {
|
onSearchResultsFillRequest: function(backwards) {
|
||||||
if (!backwards)
|
if (!backwards) {
|
||||||
return q(false);
|
return q(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.searchResults.next_batch) {
|
if (this.state.searchResults.next_batch) {
|
||||||
debuglog("requesting more search results");
|
debuglog("requesting more search results");
|
||||||
|
@ -719,15 +722,11 @@ module.exports = React.createClass({
|
||||||
if (!result.displayname) {
|
if (!result.displayname) {
|
||||||
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
|
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
|
||||||
var dialog_defer = q.defer();
|
var dialog_defer = q.defer();
|
||||||
var dialog_ref;
|
|
||||||
Modal.createDialog(SetDisplayNameDialog, {
|
Modal.createDialog(SetDisplayNameDialog, {
|
||||||
currentDisplayName: result.displayname,
|
currentDisplayName: result.displayname,
|
||||||
ref: (r) => {
|
onFinished: (submitted, newDisplayName) => {
|
||||||
dialog_ref = r;
|
|
||||||
},
|
|
||||||
onFinished: (submitted) => {
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
cli.setDisplayName(dialog_ref.getValue()).done(() => {
|
cli.setDisplayName(newDisplayName).done(() => {
|
||||||
dialog_defer.resolve();
|
dialog_defer.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -758,7 +757,7 @@ module.exports = React.createClass({
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||||
return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
|
return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
|
||||||
{ inviteSignUrl: sign_url } )
|
{ inviteSignUrl: sign_url } );
|
||||||
}).then(function(resp) {
|
}).then(function(resp) {
|
||||||
var roomId = resp.roomId;
|
var roomId = resp.roomId;
|
||||||
|
|
||||||
|
@ -810,11 +809,6 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
var msg = error.message ? error.message : JSON.stringify(error);
|
var msg = error.message ? error.message : JSON.stringify(error);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
if (msg === "No known servers") {
|
|
||||||
// minging kludge until https://matrix.org/jira/browse/SYN-678 is fixed
|
|
||||||
// 'Error when trying to join an empty room should be more explicit'
|
|
||||||
msg = "It is not currently possible to re-join an empty room.";
|
|
||||||
}
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to join room",
|
title: "Failed to join room",
|
||||||
description: msg
|
description: msg
|
||||||
|
@ -967,7 +961,7 @@ module.exports = React.createClass({
|
||||||
// For overlapping highlights,
|
// For overlapping highlights,
|
||||||
// favour longer (more specific) terms first
|
// favour longer (more specific) terms first
|
||||||
highlights = highlights.sort(function(a, b) {
|
highlights = highlights.sort(function(a, b) {
|
||||||
return b.length - a.length });
|
return b.length - a.length; });
|
||||||
|
|
||||||
self.setState({
|
self.setState({
|
||||||
searchHighlights: highlights,
|
searchHighlights: highlights,
|
||||||
|
@ -1030,7 +1024,7 @@ module.exports = React.createClass({
|
||||||
if (scrollPanel) {
|
if (scrollPanel) {
|
||||||
scrollPanel.checkScroll();
|
scrollPanel.checkScroll();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
var lastRoomId;
|
var lastRoomId;
|
||||||
|
|
||||||
|
@ -1095,7 +1089,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refs.room_settings.save().then((results) => {
|
this.refs.room_settings.save().then((results) => {
|
||||||
var fails = results.filter(function(result) { return result.state !== "fulfilled" });
|
var fails = results.filter(function(result) { return result.state !== "fulfilled"; });
|
||||||
console.log("Settings saved with %s errors", fails.length);
|
console.log("Settings saved with %s errors", fails.length);
|
||||||
if (fails.length) {
|
if (fails.length) {
|
||||||
fails.forEach(function(result) {
|
fails.forEach(function(result) {
|
||||||
|
@ -1104,7 +1098,7 @@ module.exports = React.createClass({
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to save settings",
|
title: "Failed to save settings",
|
||||||
description: fails.map(function(result) { return result.reason }).join("\n"),
|
description: fails.map(function(result) { return result.reason; }).join("\n"),
|
||||||
});
|
});
|
||||||
// still editing room settings
|
// still editing room settings
|
||||||
}
|
}
|
||||||
|
@ -1188,7 +1182,7 @@ module.exports = React.createClass({
|
||||||
this.setState({ searching: true });
|
this.setState({ searching: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancelSearchClick: function () {
|
onCancelSearchClick: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
searching: false,
|
searching: false,
|
||||||
searchResults: null,
|
searchResults: null,
|
||||||
|
@ -1213,8 +1207,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// decide whether or not the top 'unread messages' bar should be shown
|
// decide whether or not the top 'unread messages' bar should be shown
|
||||||
_updateTopUnreadMessagesBar: function() {
|
_updateTopUnreadMessagesBar: function() {
|
||||||
if (!this.refs.messagePanel)
|
if (!this.refs.messagePanel) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var pos = this.refs.messagePanel.getReadMarkerPosition();
|
var pos = this.refs.messagePanel.getReadMarkerPosition();
|
||||||
|
|
||||||
|
@ -1336,6 +1331,20 @@ module.exports = React.createClass({
|
||||||
// no longer anything to do here
|
// no longer anything to do here
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onStatusBarVisible: function() {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
this.setState({
|
||||||
|
statusBarVisible: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onStatusBarHidden: function() {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
this.setState({
|
||||||
|
statusBarVisible: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
showSettings: function(show) {
|
showSettings: function(show) {
|
||||||
// XXX: this is a bit naughty; we should be doing this via props
|
// XXX: this is a bit naughty; we should be doing this via props
|
||||||
if (show) {
|
if (show) {
|
||||||
|
@ -1500,13 +1509,14 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
var statusBar;
|
var statusBar;
|
||||||
|
let isStatusAreaExpanded = true;
|
||||||
|
|
||||||
if (ContentMessages.getCurrentUploads().length > 0) {
|
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||||
var UploadBar = sdk.getComponent('structures.UploadBar');
|
var UploadBar = sdk.getComponent('structures.UploadBar');
|
||||||
statusBar = <UploadBar room={this.state.room} />
|
statusBar = <UploadBar room={this.state.room} />;
|
||||||
} else if (!this.state.searchResults) {
|
} else if (!this.state.searchResults) {
|
||||||
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
||||||
|
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||||
statusBar = <RoomStatusBar
|
statusBar = <RoomStatusBar
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
tabComplete={this.tabComplete}
|
tabComplete={this.tabComplete}
|
||||||
|
@ -1518,7 +1528,10 @@ module.exports = React.createClass({
|
||||||
onCancelAllClick={this.onCancelAllClick}
|
onCancelAllClick={this.onCancelAllClick}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
onResize={this.onChildResize}
|
onResize={this.onChildResize}
|
||||||
/>
|
onVisible={this.onStatusBarVisible}
|
||||||
|
onHidden={this.onStatusBarHidden}
|
||||||
|
whoIsTypingLimit={2}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var aux = null;
|
var aux = null;
|
||||||
|
@ -1574,7 +1587,7 @@ module.exports = React.createClass({
|
||||||
messageComposer =
|
messageComposer =
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
|
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
|
||||||
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>
|
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Why aren't we storing the term/scope/count in this format
|
// TODO: Why aren't we storing the term/scope/count in this format
|
||||||
|
@ -1602,14 +1615,14 @@ module.exports = React.createClass({
|
||||||
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
|
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
|
||||||
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
|
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
|
||||||
width="31" height="27"/>
|
width="31" height="27"/>
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
voiceMuteButton =
|
voiceMuteButton =
|
||||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
||||||
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
|
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
|
||||||
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
|
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
|
||||||
width="21" height="26"/>
|
width="21" height="26"/>
|
||||||
</div>
|
</div>;
|
||||||
|
|
||||||
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
|
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
|
||||||
statusBar =
|
statusBar =
|
||||||
|
@ -1619,7 +1632,7 @@ module.exports = React.createClass({
|
||||||
{ zoomButton }
|
{ zoomButton }
|
||||||
{ statusBar }
|
{ statusBar }
|
||||||
<TintableSvg className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17"/>
|
<TintableSvg className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17"/>
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we have search results, we keep the messagepanel (so that it preserves its
|
// if we have search results, we keep the messagepanel (so that it preserves its
|
||||||
|
@ -1672,6 +1685,10 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||||
|
if (isStatusAreaExpanded) {
|
||||||
|
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||||
|
@ -1694,7 +1711,7 @@ module.exports = React.createClass({
|
||||||
{ topUnreadMessagesBar }
|
{ topUnreadMessagesBar }
|
||||||
{ messagePanel }
|
{ messagePanel }
|
||||||
{ searchResultsPanel }
|
{ searchResultsPanel }
|
||||||
<div className="mx_RoomView_statusArea mx_fadable" style={{ opacity: this.props.opacity }}>
|
<div className={statusBarAreaClass} style={{opacity: this.props.opacity}}>
|
||||||
<div className="mx_RoomView_statusAreaBox">
|
<div className="mx_RoomView_statusAreaBox">
|
||||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||||
{ statusBar }
|
{ statusBar }
|
||||||
|
|
|
@ -23,11 +23,18 @@ var KeyCode = require('../../KeyCode');
|
||||||
var DEBUG_SCROLL = false;
|
var DEBUG_SCROLL = false;
|
||||||
// var DEBUG_SCROLL = true;
|
// var DEBUG_SCROLL = true;
|
||||||
|
|
||||||
|
// The amount of extra scroll distance to allow prior to unfilling.
|
||||||
|
// See _getExcessHeight.
|
||||||
|
const UNPAGINATION_PADDING = 1500;
|
||||||
|
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||||
|
// many scroll events causing many unfilling requests.
|
||||||
|
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||||
|
|
||||||
if (DEBUG_SCROLL) {
|
if (DEBUG_SCROLL) {
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
var debuglog = console.log.bind(console);
|
var debuglog = console.log.bind(console);
|
||||||
} else {
|
} else {
|
||||||
var debuglog = function () {};
|
var debuglog = function() {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This component implements an intelligent scrolling list.
|
/* This component implements an intelligent scrolling list.
|
||||||
|
@ -101,6 +108,17 @@ module.exports = React.createClass({
|
||||||
*/
|
*/
|
||||||
onFillRequest: React.PropTypes.func,
|
onFillRequest: React.PropTypes.func,
|
||||||
|
|
||||||
|
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||||
|
* there are children elements that are far out of view and could be removed
|
||||||
|
* without causing pagination to occur.
|
||||||
|
*
|
||||||
|
* This function should accept a boolean, which is true to indicate the back/top
|
||||||
|
* of the panel and false otherwise, and a scroll token, which refers to the
|
||||||
|
* first element to remove if removing from the front/bottom, and last element
|
||||||
|
* to remove if removing from the back/top.
|
||||||
|
*/
|
||||||
|
onUnfillRequest: React.PropTypes.func,
|
||||||
|
|
||||||
/* onScroll: a callback which is called whenever any scroll happens.
|
/* onScroll: a callback which is called whenever any scroll happens.
|
||||||
*/
|
*/
|
||||||
onScroll: React.PropTypes.func,
|
onScroll: React.PropTypes.func,
|
||||||
|
@ -124,6 +142,7 @@ module.exports = React.createClass({
|
||||||
stickyBottom: true,
|
stickyBottom: true,
|
||||||
startAtBottom: true,
|
startAtBottom: true,
|
||||||
onFillRequest: function(backwards) { return q(false); },
|
onFillRequest: function(backwards) { return q(false); },
|
||||||
|
onUnfillRequest: function(backwards, scrollToken) {},
|
||||||
onScroll: function() {},
|
onScroll: function() {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -226,6 +245,46 @@ module.exports = React.createClass({
|
||||||
return sn.scrollHeight - Math.ceil(sn.scrollTop) <= sn.clientHeight + 3;
|
return sn.scrollHeight - Math.ceil(sn.scrollTop) <= sn.clientHeight + 3;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// returns the vertical height in the given direction that can be removed from
|
||||||
|
// the content box (which has a height of scrollHeight, see checkFillState) without
|
||||||
|
// pagination occuring.
|
||||||
|
//
|
||||||
|
// padding* = UNPAGINATION_PADDING
|
||||||
|
//
|
||||||
|
// ### Region determined as excess.
|
||||||
|
//
|
||||||
|
// .---------. - -
|
||||||
|
// |#########| | |
|
||||||
|
// |#########| - | scrollTop |
|
||||||
|
// | | | padding* | |
|
||||||
|
// | | | | |
|
||||||
|
// .-+---------+-. - - | |
|
||||||
|
// : | | : | | |
|
||||||
|
// : | | : | clientHeight | |
|
||||||
|
// : | | : | | |
|
||||||
|
// .-+---------+-. - - |
|
||||||
|
// | | | | | |
|
||||||
|
// | | | | | clientHeight | scrollHeight
|
||||||
|
// | | | | | |
|
||||||
|
// `-+---------+-' - |
|
||||||
|
// : | | : | |
|
||||||
|
// : | | : | clientHeight |
|
||||||
|
// : | | : | |
|
||||||
|
// `-+---------+-' - - |
|
||||||
|
// | | | padding* |
|
||||||
|
// | | | |
|
||||||
|
// |#########| - |
|
||||||
|
// |#########| |
|
||||||
|
// `---------' -
|
||||||
|
_getExcessHeight: function(backwards) {
|
||||||
|
var sn = this._getScrollNode();
|
||||||
|
if (backwards) {
|
||||||
|
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
||||||
|
} else {
|
||||||
|
return sn.scrollHeight - (sn.scrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// check the scroll state and send out backfill requests if necessary.
|
// check the scroll state and send out backfill requests if necessary.
|
||||||
checkFillState: function() {
|
checkFillState: function() {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
|
@ -268,6 +327,55 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// check if unfilling is possible and send an unfill request if necessary
|
||||||
|
_checkUnfillState: function(backwards) {
|
||||||
|
let excessHeight = this._getExcessHeight(backwards);
|
||||||
|
if (excessHeight <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var itemlist = this.refs.itemlist;
|
||||||
|
var tiles = itemlist.children;
|
||||||
|
|
||||||
|
// The scroll token of the first/last tile to be unpaginated
|
||||||
|
let markerScrollToken = null;
|
||||||
|
|
||||||
|
// Subtract clientHeights to simulate the events being unpaginated whilst counting
|
||||||
|
// the events to be unpaginated.
|
||||||
|
if (backwards) {
|
||||||
|
// Iterate forwards from start of tiles, subtracting event tile height
|
||||||
|
let i = 0;
|
||||||
|
while (i < tiles.length && excessHeight > tiles[i].clientHeight) {
|
||||||
|
excessHeight -= tiles[i].clientHeight;
|
||||||
|
if (tiles[i].dataset.scrollToken) {
|
||||||
|
markerScrollToken = tiles[i].dataset.scrollToken;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Iterate backwards from end of tiles, subtracting event tile height
|
||||||
|
let i = tiles.length - 1;
|
||||||
|
while (i > 0 && excessHeight > tiles[i].clientHeight) {
|
||||||
|
excessHeight -= tiles[i].clientHeight;
|
||||||
|
if (tiles[i].dataset.scrollToken) {
|
||||||
|
markerScrollToken = tiles[i].dataset.scrollToken;
|
||||||
|
}
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markerScrollToken) {
|
||||||
|
// Use a debouncer to prevent multiple unfill calls in quick succession
|
||||||
|
// This is to make the unfilling process less aggressive
|
||||||
|
if (this._unfillDebouncer) {
|
||||||
|
clearTimeout(this._unfillDebouncer);
|
||||||
|
}
|
||||||
|
this._unfillDebouncer = setTimeout(() => {
|
||||||
|
this._unfillDebouncer = null;
|
||||||
|
this.props.onUnfillRequest(backwards, markerScrollToken);
|
||||||
|
}, UNFILL_REQUEST_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// check if there is already a pending fill request. If not, set one off.
|
// check if there is already a pending fill request. If not, set one off.
|
||||||
_maybeFill: function(backwards) {
|
_maybeFill: function(backwards) {
|
||||||
var dir = backwards ? 'b' : 'f';
|
var dir = backwards ? 'b' : 'f';
|
||||||
|
@ -285,7 +393,7 @@ module.exports = React.createClass({
|
||||||
this._pendingFillRequests[dir] = true;
|
this._pendingFillRequests[dir] = true;
|
||||||
var fillPromise;
|
var fillPromise;
|
||||||
try {
|
try {
|
||||||
fillPromise = this.props.onFillRequest(backwards);
|
fillPromise = this.props.onFillRequest(backwards);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._pendingFillRequests[dir] = false;
|
this._pendingFillRequests[dir] = false;
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -294,6 +402,12 @@ module.exports = React.createClass({
|
||||||
q.finally(fillPromise, () => {
|
q.finally(fillPromise, () => {
|
||||||
this._pendingFillRequests[dir] = false;
|
this._pendingFillRequests[dir] = false;
|
||||||
}).then((hasMoreResults) => {
|
}).then((hasMoreResults) => {
|
||||||
|
if (this.unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Unpaginate once filling is complete
|
||||||
|
this._checkUnfillState(!backwards);
|
||||||
|
|
||||||
debuglog("ScrollPanel: "+dir+" fill complete; hasMoreResults:"+hasMoreResults);
|
debuglog("ScrollPanel: "+dir+" fill complete; hasMoreResults:"+hasMoreResults);
|
||||||
if (hasMoreResults) {
|
if (hasMoreResults) {
|
||||||
// further pagination requests have been disabled until now, so
|
// further pagination requests have been disabled until now, so
|
||||||
|
@ -456,7 +570,7 @@ module.exports = React.createClass({
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||||
|
|
||||||
debuglog("Scrolling to token '" + node.dataset.scrollToken + "'+" +
|
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
|
||||||
pixelOffset + " (delta: "+scrollDelta+")");
|
pixelOffset + " (delta: "+scrollDelta+")");
|
||||||
|
|
||||||
if(scrollDelta != 0) {
|
if(scrollDelta != 0) {
|
||||||
|
@ -468,7 +582,7 @@ module.exports = React.createClass({
|
||||||
_saveScrollState: function() {
|
_saveScrollState: function() {
|
||||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||||
this.scrollState = { stuckAtBottom: true };
|
this.scrollState = { stuckAtBottom: true };
|
||||||
debuglog("Saved scroll state", this.scrollState);
|
debuglog("ScrollPanel: Saved scroll state", this.scrollState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -486,13 +600,13 @@ module.exports = React.createClass({
|
||||||
stuckAtBottom: false,
|
stuckAtBottom: false,
|
||||||
trackedScrollToken: node.dataset.scrollToken,
|
trackedScrollToken: node.dataset.scrollToken,
|
||||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||||
}
|
};
|
||||||
debuglog("Saved scroll state", this.scrollState);
|
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debuglog("Unable to save scroll state: found no children in the viewport");
|
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||||
},
|
},
|
||||||
|
|
||||||
_restoreSavedScrollState: function() {
|
_restoreSavedScrollState: function() {
|
||||||
|
@ -526,7 +640,7 @@ module.exports = React.createClass({
|
||||||
this._lastSetScroll = scrollNode.scrollTop;
|
this._lastSetScroll = scrollNode.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
debuglog("Set scrollTop:", scrollNode.scrollTop,
|
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
|
||||||
"requested:", scrollTop,
|
"requested:", scrollTop,
|
||||||
"_lastSetScroll:", this._lastSetScroll);
|
"_lastSetScroll:", this._lastSetScroll);
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,7 @@ if (DEBUG) {
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
var debuglog = console.log.bind(console);
|
var debuglog = console.log.bind(console);
|
||||||
} else {
|
} else {
|
||||||
var debuglog = function () {};
|
var debuglog = function() {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -108,7 +108,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
timelineCap: 250,
|
// By default, disable the timelineCap in favour of unpaginating based on
|
||||||
|
// event tile heights. (See _unpaginateEvents)
|
||||||
|
timelineCap: Number.MAX_VALUE,
|
||||||
className: 'mx_RoomView_messagePanel',
|
className: 'mx_RoomView_messagePanel',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -245,6 +247,34 @@ var TimelinePanel = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
||||||
|
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
|
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
||||||
|
|
||||||
|
// All tiles are inserted by MessagePanel to have a scrollToken === eventId
|
||||||
|
let eventId = scrollToken;
|
||||||
|
|
||||||
|
let marker = this.state.events.findIndex(
|
||||||
|
(ev) => {
|
||||||
|
return ev.getId() === eventId;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let count = backwards ? marker + 1 : this.state.events.length - marker;
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
|
||||||
|
this._timelineWindow.unpaginate(count, backwards);
|
||||||
|
|
||||||
|
// We can now paginate in the unpaginated direction
|
||||||
|
const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate';
|
||||||
|
this.setState({
|
||||||
|
[canPaginateKey]: true,
|
||||||
|
events: this._getEvents(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// set off a pagination request.
|
// set off a pagination request.
|
||||||
onMessageListFillRequest: function(backwards) {
|
onMessageListFillRequest: function(backwards) {
|
||||||
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
|
@ -292,7 +322,7 @@ var TimelinePanel = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageListScroll: function () {
|
onMessageListScroll: function() {
|
||||||
if (this.props.onScroll) {
|
if (this.props.onScroll) {
|
||||||
this.props.onScroll();
|
this.props.onScroll();
|
||||||
}
|
}
|
||||||
|
@ -357,7 +387,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// if we're at the end of the live timeline, append the pending events
|
// if we're at the end of the live timeline, append the pending events
|
||||||
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
events.push(... this.props.timelineSet.room.getPendingEvents());
|
events.push(...this.props.timelineSet.room.getPendingEvents());
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedState = {events: events};
|
var updatedState = {events: events};
|
||||||
|
@ -534,8 +564,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// first find where the current RM is
|
// first find where the current RM is
|
||||||
for (var i = 0; i < events.length; i++) {
|
for (var i = 0; i < events.length; i++) {
|
||||||
if (events[i].getId() == this.state.readMarkerEventId)
|
if (events[i].getId() == this.state.readMarkerEventId) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (i >= events.length) {
|
if (i >= events.length) {
|
||||||
return;
|
return;
|
||||||
|
@ -614,7 +645,7 @@ var TimelinePanel = React.createClass({
|
||||||
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
||||||
var rmTs;
|
var rmTs;
|
||||||
if (tl) {
|
if (tl) {
|
||||||
var event = tl.getEvents().find((e) => { return e.getId() == rmId });
|
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
||||||
if (event) {
|
if (event) {
|
||||||
rmTs = event.getTs();
|
rmTs = event.getTs();
|
||||||
}
|
}
|
||||||
|
@ -780,7 +811,7 @@ var TimelinePanel = React.createClass({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var message = "Riot was trying to load a specific point in this room's timeline but ";
|
var message = "Tried to load a specific point in this room's timeline, but ";
|
||||||
if (error.errcode == 'M_FORBIDDEN') {
|
if (error.errcode == 'M_FORBIDDEN') {
|
||||||
message += "you do not have permission to view the message in question.";
|
message += "you do not have permission to view the message in question.";
|
||||||
} else {
|
} else {
|
||||||
|
@ -791,7 +822,7 @@ var TimelinePanel = React.createClass({
|
||||||
description: message,
|
description: message,
|
||||||
onFinished: onFinished,
|
onFinished: onFinished,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||||
|
|
||||||
|
@ -813,7 +844,7 @@ var TimelinePanel = React.createClass({
|
||||||
timelineLoading: true,
|
timelineLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
prom = prom.then(onLoaded, onError)
|
prom = prom.then(onLoaded, onError);
|
||||||
}
|
}
|
||||||
|
|
||||||
prom.done();
|
prom.done();
|
||||||
|
@ -838,7 +869,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// if we're at the end of the live timeline, append the pending events
|
// if we're at the end of the live timeline, append the pending events
|
||||||
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
events.push(... this.props.timelineSet.getPendingEvents());
|
events.push(...this.props.timelineSet.getPendingEvents());
|
||||||
}
|
}
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
|
@ -900,8 +931,9 @@ var TimelinePanel = React.createClass({
|
||||||
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
// the client can be null on logout
|
// the client can be null on logout
|
||||||
if (client == null)
|
if (client == null) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var myUserId = client.credentials.userId;
|
var myUserId = client.credentials.userId;
|
||||||
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||||
|
@ -984,6 +1016,7 @@ var TimelinePanel = React.createClass({
|
||||||
stickyBottom={ stickyBottom }
|
stickyBottom={ stickyBottom }
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
onFillRequest={ this.onMessageListFillRequest }
|
onFillRequest={ this.onMessageListFillRequest }
|
||||||
|
onUnfillRequest={ this.onMessageListUnfillRequest }
|
||||||
opacity={ this.props.opacity }
|
opacity={ this.props.opacity }
|
||||||
className={ this.props.className }
|
className={ this.props.className }
|
||||||
tileShape={ this.props.tileShape }
|
tileShape={ this.props.tileShape }
|
||||||
|
|
|
@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
// }];
|
// }];
|
||||||
|
|
||||||
if (uploads.length == 0) {
|
if (uploads.length == 0) {
|
||||||
return <div />
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var upload;
|
var upload;
|
||||||
|
@ -68,7 +68,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!upload) {
|
if (!upload) {
|
||||||
return <div />
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var innerProgressStyle = {
|
var innerProgressStyle = {
|
||||||
|
@ -76,7 +76,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
};
|
};
|
||||||
var uploadedSize = filesize(upload.loaded);
|
var uploadedSize = filesize(upload.loaded);
|
||||||
var totalSize = filesize(upload.total);
|
var totalSize = filesize(upload.total);
|
||||||
if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) {
|
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
|
||||||
uploadedSize = uploadedSize.replace(/ .*/, '');
|
uploadedSize = uploadedSize.replace(/ .*/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,12 +26,61 @@ var UserSettingsStore = require('../../UserSettingsStore');
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
var Email = require('../../email');
|
var Email = require('../../email');
|
||||||
var AddThreepid = require('../../AddThreepid');
|
var AddThreepid = require('../../AddThreepid');
|
||||||
|
var SdkConfig = require('../../SdkConfig');
|
||||||
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
|
|
||||||
// if this looks like a release, use the 'version' from package.json; else use
|
// if this looks like a release, use the 'version' from package.json; else use
|
||||||
// the git sha.
|
// the git sha.
|
||||||
const REACT_SDK_VERSION =
|
const REACT_SDK_VERSION =
|
||||||
'dist' in package_json ? package_json.version : package_json.gitHead || "<local>";
|
'dist' in package_json ? package_json.version : package_json.gitHead || "<local>";
|
||||||
|
|
||||||
|
|
||||||
|
// Enumerate some simple 'flip a bit' UI settings (if any).
|
||||||
|
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||||
|
// 'label' is how we describe it in the UI.
|
||||||
|
const SETTINGS_LABELS = [
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
id: 'alwaysShowTimestamps',
|
||||||
|
label: 'Always show message timestamps',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'showTwelveHourTimestamps',
|
||||||
|
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'useCompactLayout',
|
||||||
|
label: 'Use compact timeline layout',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'useFixedWidthFont',
|
||||||
|
label: 'Use fixed width font',
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
];
|
||||||
|
|
||||||
|
// Enumerate the available themes, with a nice human text label.
|
||||||
|
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||||
|
// 'value' is the value for that key in the event
|
||||||
|
// 'label' is how we describe it in the UI.
|
||||||
|
//
|
||||||
|
// XXX: Ideally we would have a theme manifest or something and they'd be nicely
|
||||||
|
// packaged up in a single directory, and/or located at the application layer.
|
||||||
|
// But for now for expedience we just hardcode them here.
|
||||||
|
const THEMES = [
|
||||||
|
{
|
||||||
|
id: 'theme',
|
||||||
|
label: 'Light theme',
|
||||||
|
value: 'light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme',
|
||||||
|
label: 'Dark theme',
|
||||||
|
value: 'dark',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'UserSettings',
|
displayName: 'UserSettings',
|
||||||
|
|
||||||
|
@ -43,6 +92,9 @@ module.exports = React.createClass({
|
||||||
// True to show the 'labs' section of experimental features
|
// True to show the 'labs' section of experimental features
|
||||||
enableLabs: React.PropTypes.bool,
|
enableLabs: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||||
|
referralBaseUrl: React.PropTypes.string,
|
||||||
|
|
||||||
// true if RightPanel is collapsed
|
// true if RightPanel is collapsed
|
||||||
collapsedRhs: React.PropTypes.bool,
|
collapsedRhs: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
@ -61,6 +113,7 @@ module.exports = React.createClass({
|
||||||
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
||||||
email_add_pending: false,
|
email_add_pending: false,
|
||||||
vectorVersion: null,
|
vectorVersion: null,
|
||||||
|
rejectingInvites: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -80,12 +133,24 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bulk rejecting invites:
|
||||||
|
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
|
||||||
|
// will still return rooms with invites. To get around this, add a listener for
|
||||||
|
// membership updates and kick the UI.
|
||||||
|
MatrixClientPeg.get().on("RoomMember.membership", this._onInviteStateChange);
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'ui_opacity',
|
action: 'ui_opacity',
|
||||||
sideOpacity: 0.3,
|
sideOpacity: 0.3,
|
||||||
middleOpacity: 0.3,
|
middleOpacity: 0.3,
|
||||||
});
|
});
|
||||||
this._refreshFromServer();
|
this._refreshFromServer();
|
||||||
|
|
||||||
|
var syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
|
if (!syncedSettings.theme) {
|
||||||
|
syncedSettings.theme = 'light';
|
||||||
|
}
|
||||||
|
this._syncedSettings = syncedSettings;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -101,6 +166,10 @@ module.exports = React.createClass({
|
||||||
middleOpacity: 1.0,
|
middleOpacity: 1.0,
|
||||||
});
|
});
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
let cli = MatrixClientPeg.get();
|
||||||
|
if (cli) {
|
||||||
|
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_refreshFromServer: function() {
|
_refreshFromServer: function() {
|
||||||
|
@ -164,8 +233,26 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onLogoutClicked: function(ev) {
|
onLogoutClicked: function(ev) {
|
||||||
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
this.logoutModal = Modal.createDialog(LogoutPrompt);
|
Modal.createDialog(QuestionDialog, {
|
||||||
|
title: "Sign out?",
|
||||||
|
description:
|
||||||
|
<div>
|
||||||
|
For security, logging out will delete any end-to-end encryption keys from this browser,
|
||||||
|
making previous encrypted chat history unreadable if you log back in.
|
||||||
|
In future this <a href="https://github.com/vector-im/riot-web/issues/2108">will be improved</a>,
|
||||||
|
but for now be warned.
|
||||||
|
</div>,
|
||||||
|
button: "Sign out",
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
dis.dispatch({action: 'logout'});
|
||||||
|
if (this.props.onFinished) {
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onPasswordChangeError: function(err) {
|
onPasswordChangeError: function(err) {
|
||||||
|
@ -237,6 +324,31 @@ module.exports = React.createClass({
|
||||||
this.setState({email_add_pending: true});
|
this.setState({email_add_pending: true});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRemoveThreepidClicked: function(threepid) {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
Modal.createDialog(QuestionDialog, {
|
||||||
|
title: "Remove Contact Information?",
|
||||||
|
description: "Remove " + threepid.address + "?",
|
||||||
|
button: 'Remove',
|
||||||
|
onFinished: (submit) => {
|
||||||
|
if (submit) {
|
||||||
|
this.setState({
|
||||||
|
phase: "UserSettings.LOADING",
|
||||||
|
});
|
||||||
|
MatrixClientPeg.get().deleteThreePid(threepid.medium, threepid.address).then(() => {
|
||||||
|
return this._refreshFromServer();
|
||||||
|
}).catch((err) => {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Unable to remove contact information",
|
||||||
|
description: err.toString(),
|
||||||
|
});
|
||||||
|
}).done();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onEmailDialogFinished: function(ok) {
|
onEmailDialogFinished: function(ok) {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
this.verifyEmailAddress();
|
this.verifyEmailAddress();
|
||||||
|
@ -257,8 +369,8 @@ module.exports = React.createClass({
|
||||||
this.setState({email_add_pending: false});
|
this.setState({email_add_pending: false});
|
||||||
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
var message = "Unable to verify email address. "
|
var message = "Unable to verify email address. ";
|
||||||
message += "Please check your email and click on the link it contains. Once this is done, click continue."
|
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Verification Pending",
|
title: "Verification Pending",
|
||||||
description: message,
|
description: message,
|
||||||
|
@ -280,91 +392,185 @@ module.exports = React.createClass({
|
||||||
Modal.createDialog(DeactivateAccountDialog, {});
|
Modal.createDialog(DeactivateAccountDialog, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onBugReportClicked: function() {
|
||||||
|
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||||
|
if (!BugReportDialog) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal.createDialog(BugReportDialog, {});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onInviteStateChange: function(event, member, oldMembership) {
|
||||||
|
if (member.userId === this._me && oldMembership === "invite") {
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRejectAllInvitesClicked: function(rooms, ev) {
|
||||||
|
this.setState({
|
||||||
|
rejectingInvites: true
|
||||||
|
});
|
||||||
|
// reject the invites
|
||||||
|
let promises = rooms.map((room) => {
|
||||||
|
return MatrixClientPeg.get().leave(room.roomId);
|
||||||
|
});
|
||||||
|
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
|
||||||
|
// after trying to reject all the invites.
|
||||||
|
q.allSettled(promises).then(() => {
|
||||||
|
this.setState({
|
||||||
|
rejectingInvites: false
|
||||||
|
});
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onExportE2eKeysClicked: function() {
|
||||||
|
Modal.createDialogAsync(
|
||||||
|
(cb) => {
|
||||||
|
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||||
|
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||||
|
}, "e2e-export");
|
||||||
|
}, {
|
||||||
|
matrixClient: MatrixClientPeg.get(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onImportE2eKeysClicked: function() {
|
||||||
|
Modal.createDialogAsync(
|
||||||
|
(cb) => {
|
||||||
|
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
|
||||||
|
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
|
||||||
|
}, "e2e-export");
|
||||||
|
}, {
|
||||||
|
matrixClient: MatrixClientPeg.get(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderReferral: function() {
|
||||||
|
const teamToken = window.localStorage.getItem('mx_team_token');
|
||||||
|
if (!teamToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof teamToken !== 'string') {
|
||||||
|
console.warn('Team token not a string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const href = (this.props.referralBaseUrl || window.location.origin) +
|
||||||
|
`/#/register?referrer=${this._me}&team_token=${teamToken}`;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Referral</h3>
|
||||||
|
<div className="mx_UserSettings_section">
|
||||||
|
Refer a friend to Riot: <a href={href}>{href}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
_renderUserInterfaceSettings: function() {
|
_renderUserInterfaceSettings: function() {
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
|
|
||||||
var settingsLabels = [
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
id: 'alwaysShowTimestamps',
|
|
||||||
label: 'Always show message timestamps',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'showTwelveHourTimestamps',
|
|
||||||
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'useCompactLayout',
|
|
||||||
label: 'Use compact timeline layout',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'useFixedWidthFont',
|
|
||||||
label: 'Use fixed width font',
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
];
|
|
||||||
|
|
||||||
var syncedSettings = UserSettingsStore.getSyncedSettings();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>User Interface</h3>
|
<h3>User Interface</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<div className="mx_UserSettings_toggle">
|
{ this._renderUrlPreviewSelector() }
|
||||||
<input id="urlPreviewsDisabled"
|
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
||||||
type="checkbox"
|
{ THEMES.map( this._renderThemeSelector ) }
|
||||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
|
||||||
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
|
|
||||||
/>
|
|
||||||
<label htmlFor="urlPreviewsDisabled">
|
|
||||||
Disable inline URL previews by default
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{ settingsLabels.forEach( setting => {
|
|
||||||
<div className="mx_UserSettings_toggle">
|
|
||||||
<input id={ setting.id }
|
|
||||||
type="checkbox"
|
|
||||||
defaultChecked={ syncedSettings[setting.id] }
|
|
||||||
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
|
|
||||||
/>
|
|
||||||
<label htmlFor={ setting.id }>
|
|
||||||
{ settings.label }
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderUrlPreviewSelector: function() {
|
||||||
|
return <div className="mx_UserSettings_toggle">
|
||||||
|
<input id="urlPreviewsDisabled"
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
||||||
|
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
|
||||||
|
/>
|
||||||
|
<label htmlFor="urlPreviewsDisabled">
|
||||||
|
Disable inline URL previews by default
|
||||||
|
</label>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderSyncedSetting: function(setting) {
|
||||||
|
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||||
|
<input id={ setting.id }
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={ this._syncedSettings[setting.id] }
|
||||||
|
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
|
||||||
|
/>
|
||||||
|
<label htmlFor={ setting.id }>
|
||||||
|
{ setting.label }
|
||||||
|
</label>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderThemeSelector: function(setting) {
|
||||||
|
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
|
||||||
|
<input id={ setting.id + "_" + setting.value }
|
||||||
|
type="radio"
|
||||||
|
name={ setting.id }
|
||||||
|
value={ setting.value }
|
||||||
|
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
||||||
|
onChange={ e => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||||
|
}
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'set_theme',
|
||||||
|
value: setting.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label htmlFor={ setting.id + "_" + setting.value }>
|
||||||
|
{ setting.label }
|
||||||
|
</label>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
_renderCryptoInfo: function() {
|
_renderCryptoInfo: function() {
|
||||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
const client = MatrixClientPeg.get();
|
||||||
return null;
|
const deviceId = client.deviceId;
|
||||||
|
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
||||||
|
|
||||||
|
let exportButton = null,
|
||||||
|
importButton = null;
|
||||||
|
|
||||||
|
if (client.isCryptoEnabled) {
|
||||||
|
exportButton = (
|
||||||
|
<AccessibleButton className="mx_UserSettings_button"
|
||||||
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
|
Export E2E room keys
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
importButton = (
|
||||||
|
<AccessibleButton className="mx_UserSettings_button"
|
||||||
|
onClick={this._onImportE2eKeysClicked}>
|
||||||
|
Import E2E room keys
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = MatrixClientPeg.get();
|
|
||||||
var deviceId = client.deviceId;
|
|
||||||
var identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
|
||||||
|
|
||||||
var myDevice = client.getStoredDevicesForUser(MatrixClientPeg.get().credentials.userId)[0];
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>Cryptography</h3>
|
<h3>Cryptography</h3>
|
||||||
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
||||||
<ul>
|
<ul>
|
||||||
<li><label>Device name:</label> <span>{ myDevice.getDisplayName() }</span></li>
|
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
|
||||||
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
|
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
|
||||||
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
{exportButton}
|
||||||
|
{importButton}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderDevicesPanel: function() {
|
_renderDevicesPanel: function() {
|
||||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -374,7 +580,24 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderLabs: function () {
|
_renderBugReport: function() {
|
||||||
|
if (!SdkConfig.get().bug_report_endpoint_url) {
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Bug Report</h3>
|
||||||
|
<div className="mx_UserSettings_section">
|
||||||
|
<p>Found a bug?</p>
|
||||||
|
<button className="mx_UserSettings_button danger"
|
||||||
|
onClick={this._onBugReportClicked}>Report it
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderLabs: function() {
|
||||||
// default to enabled if undefined
|
// default to enabled if undefined
|
||||||
if (this.props.enableLabs === false) return null;
|
if (this.props.enableLabs === false) return null;
|
||||||
|
|
||||||
|
@ -410,7 +633,7 @@ module.exports = React.createClass({
|
||||||
{features}
|
{features}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderDeactivateAccount: function() {
|
_renderDeactivateAccount: function() {
|
||||||
|
@ -420,15 +643,49 @@ module.exports = React.createClass({
|
||||||
return <div>
|
return <div>
|
||||||
<h3>Deactivate Account</h3>
|
<h3>Deactivate Account</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<button className="mx_UserSettings_button danger"
|
<AccessibleButton className="mx_UserSettings_button danger"
|
||||||
onClick={this._onDeactivateAccountClicked}>Deactivate my account
|
onClick={this._onDeactivateAccountClicked}>Deactivate my account
|
||||||
</button>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderBulkOptions: function() {
|
||||||
|
let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
|
||||||
|
return r.hasMembershipState(this._me, "invite");
|
||||||
|
});
|
||||||
|
if (invitedRooms.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
let reject = <Spinner />;
|
||||||
|
if (!this.state.rejectingInvites) {
|
||||||
|
// bind() the invited rooms so any new invites that may come in as this button is clicked
|
||||||
|
// don't inadvertently get rejected as well.
|
||||||
|
reject = (
|
||||||
|
<AccessibleButton className="mx_UserSettings_button danger"
|
||||||
|
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
|
||||||
|
Reject all {invitedRooms.length} invites
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<h3>Bulk Options</h3>
|
||||||
|
<div className="mx_UserSettings_section">
|
||||||
|
{reject}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
nameForMedium: function(medium) {
|
||||||
|
if (medium == 'msisdn') return 'Phone';
|
||||||
|
return medium[0].toUpperCase() + medium.slice(1);
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
case "UserSettings.LOADING":
|
case "UserSettings.LOADING":
|
||||||
|
@ -452,15 +709,18 @@ module.exports = React.createClass({
|
||||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
var threepidsSection = this.state.threepids.map(function(val, pidIndex) {
|
var threepidsSection = this.state.threepids.map((val, pidIndex) => {
|
||||||
var id = "email-" + val.address;
|
const id = "3pid-" + val.address;
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
<label htmlFor={id}>Email</label>
|
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<input key={val.address} id={id} value={val.address} disabled />
|
<input type="text" key={val.address} id={id} value={val.address} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
|
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -482,7 +742,7 @@ module.exports = React.createClass({
|
||||||
blurToCancel={ false }
|
blurToCancel={ false }
|
||||||
onValueChanged={ this.onAddThreepidClicked } />
|
onValueChanged={ this.onAddThreepidClicked } />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_addThreepid">
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
|
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -563,7 +823,7 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_avatarPicker_edit">
|
<div className="mx_UserSettings_avatarPicker_edit">
|
||||||
<label htmlFor="avatarInput" ref="file_label">
|
<label htmlFor="avatarInput" ref="file_label">
|
||||||
<img src="img/camera.svg"
|
<img src="img/camera.svg" className="mx_filterFlipColor"
|
||||||
alt="Upload avatar" title="Upload avatar"
|
alt="Upload avatar" title="Upload avatar"
|
||||||
width="17" height="15" />
|
width="17" height="15" />
|
||||||
</label>
|
</label>
|
||||||
|
@ -576,19 +836,23 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
|
|
||||||
<div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||||
Sign out
|
Sign out
|
||||||
</div>
|
</AccessibleButton>
|
||||||
|
|
||||||
{accountJsx}
|
{accountJsx}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this._renderReferral()}
|
||||||
|
|
||||||
{notification_area}
|
{notification_area}
|
||||||
|
|
||||||
{this._renderUserInterfaceSettings()}
|
{this._renderUserInterfaceSettings()}
|
||||||
{this._renderLabs()}
|
{this._renderLabs()}
|
||||||
{this._renderDevicesPanel()}
|
{this._renderDevicesPanel()}
|
||||||
{this._renderCryptoInfo()}
|
{this._renderCryptoInfo()}
|
||||||
|
{this._renderBulkOptions()}
|
||||||
|
{this._renderBugReport()}
|
||||||
|
|
||||||
<h3>Advanced</h3>
|
<h3>Advanced</h3>
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ module.exports = React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
progress: null
|
progress: null
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onVerify: function(ev) {
|
onVerify: function(ev) {
|
||||||
|
@ -71,7 +71,7 @@ module.exports = React.createClass({
|
||||||
this.setState({ progress: "complete" });
|
this.setState({ progress: "complete" });
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.showErrorDialog(err.message);
|
this.showErrorDialog(err.message);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmitForm: function(ev) {
|
onSubmitForm: function(ev) {
|
||||||
|
@ -87,10 +87,26 @@ module.exports = React.createClass({
|
||||||
this.showErrorDialog("New passwords must match each other.");
|
this.showErrorDialog("New passwords must match each other.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.submitPasswordReset(
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
Modal.createDialog(QuestionDialog, {
|
||||||
this.state.email, this.state.password
|
title: "Warning",
|
||||||
);
|
description:
|
||||||
|
<div>
|
||||||
|
Resetting password will currently reset any end-to-end encryption keys on all devices,
|
||||||
|
making encrypted chat history unreadable.
|
||||||
|
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">may be improved</a>,
|
||||||
|
but for now be warned.
|
||||||
|
</div>,
|
||||||
|
button: "Continue",
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.submitPasswordReset(
|
||||||
|
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||||
|
this.state.email, this.state.password
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -129,7 +145,7 @@ module.exports = React.createClass({
|
||||||
var resetPasswordJsx;
|
var resetPasswordJsx;
|
||||||
|
|
||||||
if (this.state.progress === "sending_email") {
|
if (this.state.progress === "sending_email") {
|
||||||
resetPasswordJsx = <Spinner />
|
resetPasswordJsx = <Spinner />;
|
||||||
}
|
}
|
||||||
else if (this.state.progress === "sent_email") {
|
else if (this.state.progress === "sent_email") {
|
||||||
resetPasswordJsx = (
|
resetPasswordJsx = (
|
||||||
|
|
|
@ -173,7 +173,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getCurrentFlowStep: function() {
|
_getCurrentFlowStep: function() {
|
||||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null
|
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
_setStateFromError: function(err, isLoginAttempt) {
|
_setStateFromError: function(err, isLoginAttempt) {
|
||||||
|
@ -195,7 +195,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorText = "Error: Problem communicating with the given homeserver " +
|
let errorText = "Error: Problem communicating with the given homeserver " +
|
||||||
(errCode ? "(" + errCode + ")" : "")
|
(errCode ? "(" + errCode + ")" : "");
|
||||||
|
|
||||||
if (err.cors === 'rejected') {
|
if (err.cors === 'rejected') {
|
||||||
if (window.location.protocol === 'https:' &&
|
if (window.location.protocol === 'https:' &&
|
||||||
|
@ -203,7 +203,7 @@ module.exports = React.createClass({
|
||||||
!this.state.enteredHomeserverUrl.startsWith("http")))
|
!this.state.enteredHomeserverUrl.startsWith("http")))
|
||||||
{
|
{
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
Can't connect to homeserver via HTTP when using Riot served by HTTPS.
|
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar.
|
||||||
Either use HTTPS or <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>enable unsafe scripts</a>
|
Either use HTTPS or <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>enable unsafe scripts</a>
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
@ -258,7 +258,7 @@ module.exports = React.createClass({
|
||||||
loginAsGuestJsx =
|
loginAsGuestJsx =
|
||||||
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
||||||
Login as guest
|
Login as guest
|
||||||
</a>
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var returnToAppJsx;
|
var returnToAppJsx;
|
||||||
|
@ -266,7 +266,7 @@ module.exports = React.createClass({
|
||||||
returnToAppJsx =
|
returnToAppJsx =
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||||
Return to app
|
Return to app
|
||||||
</a>
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig");
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var RegistrationForm = require("../../views/login/RegistrationForm");
|
var RegistrationForm = require("../../views/login/RegistrationForm");
|
||||||
var CaptchaForm = require("../../views/login/CaptchaForm");
|
var CaptchaForm = require("../../views/login/CaptchaForm");
|
||||||
|
var RtsClient = require("../../../RtsClient");
|
||||||
|
|
||||||
var MIN_PASSWORD_LENGTH = 6;
|
var MIN_PASSWORD_LENGTH = 6;
|
||||||
|
|
||||||
|
@ -47,8 +48,16 @@ module.exports = React.createClass({
|
||||||
defaultIsUrl: React.PropTypes.string,
|
defaultIsUrl: React.PropTypes.string,
|
||||||
brand: React.PropTypes.string,
|
brand: React.PropTypes.string,
|
||||||
email: React.PropTypes.string,
|
email: React.PropTypes.string,
|
||||||
|
referrer: React.PropTypes.string,
|
||||||
username: React.PropTypes.string,
|
username: React.PropTypes.string,
|
||||||
guestAccessToken: React.PropTypes.string,
|
guestAccessToken: React.PropTypes.string,
|
||||||
|
teamServerConfig: React.PropTypes.shape({
|
||||||
|
// Email address to request new teams
|
||||||
|
supportEmail: React.PropTypes.string.isRequired,
|
||||||
|
// URL of the riot-team-server to get team configurations and track referrals
|
||||||
|
teamServerURL: React.PropTypes.string.isRequired,
|
||||||
|
}),
|
||||||
|
teamSelected: React.PropTypes.object,
|
||||||
|
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: React.PropTypes.string,
|
||||||
|
|
||||||
|
@ -60,6 +69,7 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
busy: false,
|
busy: false,
|
||||||
|
teamServerBusy: false,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
// We remember the values entered by the user because
|
// We remember the values entered by the user because
|
||||||
// the registration form will be unmounted during the
|
// the registration form will be unmounted during the
|
||||||
|
@ -75,6 +85,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
// attach this to the instance rather than this.state since it isn't UI
|
// attach this to the instance rather than this.state since it isn't UI
|
||||||
this.registerLogic = new Signup.Register(
|
this.registerLogic = new Signup.Register(
|
||||||
|
@ -88,10 +99,40 @@ module.exports = React.createClass({
|
||||||
this.registerLogic.setIdSid(this.props.idSid);
|
this.registerLogic.setIdSid(this.props.idSid);
|
||||||
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||||
this.registerLogic.recheckState();
|
this.registerLogic.recheckState();
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.props.teamServerConfig &&
|
||||||
|
this.props.teamServerConfig.teamServerURL &&
|
||||||
|
!this._rtsClient
|
||||||
|
) {
|
||||||
|
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
teamServerBusy: true,
|
||||||
|
});
|
||||||
|
// GET team configurations including domains, names and icons
|
||||||
|
this._rtsClient.getTeamsConfig().then((data) => {
|
||||||
|
const teamsConfig = {
|
||||||
|
teams: data,
|
||||||
|
supportEmail: this.props.teamServerConfig.supportEmail,
|
||||||
|
};
|
||||||
|
console.log('Setting teams config to ', teamsConfig);
|
||||||
|
this.setState({
|
||||||
|
teamsConfig: teamsConfig,
|
||||||
|
teamServerBusy: false,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
console.error('Error retrieving config for teams', err);
|
||||||
|
this.setState({
|
||||||
|
teamServerBusy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
this._unmounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -169,6 +210,43 @@ module.exports = React.createClass({
|
||||||
accessToken: response.access_token
|
accessToken: response.access_token
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
self._rtsClient &&
|
||||||
|
self.props.referrer &&
|
||||||
|
self.state.teamSelected
|
||||||
|
) {
|
||||||
|
// Track referral, get team_token in order to retrieve team config
|
||||||
|
self._rtsClient.trackReferral(
|
||||||
|
self.props.referrer,
|
||||||
|
response.user_id,
|
||||||
|
self.state.formVals.email
|
||||||
|
).then((data) => {
|
||||||
|
const teamToken = data.team_token;
|
||||||
|
// Store for use /w welcome pages
|
||||||
|
window.localStorage.setItem('mx_team_token', teamToken);
|
||||||
|
|
||||||
|
self._rtsClient.getTeam(teamToken).then((team) => {
|
||||||
|
console.log(
|
||||||
|
`User successfully registered with team ${team.name}`
|
||||||
|
);
|
||||||
|
if (!team.rooms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Auto-join rooms
|
||||||
|
team.rooms.forEach((room) => {
|
||||||
|
if (room.auto_join && room.room_id) {
|
||||||
|
console.log(`Auto-joining ${room.room_id}`);
|
||||||
|
MatrixClientPeg.get().joinRoom(room.room_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
console.error('Error getting team config', err);
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
console.error('Error tracking referral', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (self.props.brand) {
|
if (self.props.brand) {
|
||||||
MatrixClientPeg.get().getPushers().done((resp)=>{
|
MatrixClientPeg.get().getPushers().done((resp)=>{
|
||||||
var pushers = resp.pushers;
|
var pushers = resp.pushers;
|
||||||
|
@ -238,7 +316,15 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onTeamSelected: function(teamSelected) {
|
||||||
|
if (!this._unmounted) {
|
||||||
|
this.setState({ teamSelected });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_getRegisterContentJsx: function() {
|
_getRegisterContentJsx: function() {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
var currStep = this.registerLogic.getStep();
|
var currStep = this.registerLogic.getStep();
|
||||||
var registerStep;
|
var registerStep;
|
||||||
switch (currStep) {
|
switch (currStep) {
|
||||||
|
@ -248,16 +334,23 @@ module.exports = React.createClass({
|
||||||
case "Register.STEP_m.login.dummy":
|
case "Register.STEP_m.login.dummy":
|
||||||
// NB. Our 'username' prop is specifically for upgrading
|
// NB. Our 'username' prop is specifically for upgrading
|
||||||
// a guest account
|
// a guest account
|
||||||
|
if (this.state.teamServerBusy) {
|
||||||
|
registerStep = <Spinner />;
|
||||||
|
break;
|
||||||
|
}
|
||||||
registerStep = (
|
registerStep = (
|
||||||
<RegistrationForm
|
<RegistrationForm
|
||||||
showEmail={true}
|
showEmail={true}
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
defaultPassword={this.state.formVals.password}
|
defaultPassword={this.state.formVals.password}
|
||||||
|
teamsConfig={this.state.teamsConfig}
|
||||||
guestUsername={this.props.username}
|
guestUsername={this.props.username}
|
||||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
onError={this.onFormValidationFailed}
|
onError={this.onFormValidationFailed}
|
||||||
onRegisterClick={this.onFormSubmit} />
|
onRegisterClick={this.onFormSubmit}
|
||||||
|
onTeamSelected={this.onTeamSelected}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "Register.STEP_m.login.email.identity":
|
case "Register.STEP_m.login.email.identity":
|
||||||
|
@ -273,6 +366,7 @@ module.exports = React.createClass({
|
||||||
if (serverParams && serverParams["m.login.recaptcha"]) {
|
if (serverParams && serverParams["m.login.recaptcha"]) {
|
||||||
publicKey = serverParams["m.login.recaptcha"].public_key;
|
publicKey = serverParams["m.login.recaptcha"].public_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerStep = (
|
registerStep = (
|
||||||
<CaptchaForm sitePublicKey={publicKey}
|
<CaptchaForm sitePublicKey={publicKey}
|
||||||
onCaptchaResponse={this.onCaptchaResponse}
|
onCaptchaResponse={this.onCaptchaResponse}
|
||||||
|
@ -285,7 +379,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
var busySpinner;
|
var busySpinner;
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
var Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
busySpinner = (
|
busySpinner = (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
);
|
);
|
||||||
|
@ -296,7 +389,7 @@ module.exports = React.createClass({
|
||||||
returnToAppJsx =
|
returnToAppJsx =
|
||||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||||
Return to app
|
Return to app
|
||||||
</a>
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -330,7 +423,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login">
|
<div className="mx_Login">
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/>
|
||||||
{this._getRegisterContentJsx()}
|
{this._getRegisterContentJsx()}
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var AvatarLogic = require("../../../Avatar");
|
var AvatarLogic = require("../../../Avatar");
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'BaseAvatar',
|
displayName: 'BaseAvatar',
|
||||||
|
@ -41,7 +42,7 @@ module.exports = React.createClass({
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
defaultToInitialLetter: true
|
defaultToInitialLetter: true
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -138,7 +139,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name, idName, title, url, urls, width, height, resizeMethod,
|
name, idName, title, url, urls, width, height, resizeMethod,
|
||||||
defaultToInitialLetter, viewUserOnClick,
|
defaultToInitialLetter, onClick,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -156,12 +157,24 @@ module.exports = React.createClass({
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
if (onClick != null) {
|
||||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
return (
|
||||||
onError={this.onError}
|
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
|
||||||
width={width} height={height}
|
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||||
title={title} alt=""
|
onError={this.onError}
|
||||||
{...otherProps} />
|
width={width} height={height}
|
||||||
);
|
title={title} alt=""
|
||||||
|
{...otherProps} />
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||||
|
onError={this.onError}
|
||||||
|
width={width} height={height}
|
||||||
|
title={title} alt=""
|
||||||
|
{...otherProps} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,6 +33,7 @@ module.exports = React.createClass({
|
||||||
onClick: React.PropTypes.func,
|
onClick: React.PropTypes.func,
|
||||||
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
||||||
viewUserOnClick: React.PropTypes.bool,
|
viewUserOnClick: React.PropTypes.bool,
|
||||||
|
title: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -41,7 +42,7 @@ module.exports = React.createClass({
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
viewUserOnClick: false,
|
viewUserOnClick: false,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -58,26 +59,26 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: props.member.name,
|
name: props.member.name,
|
||||||
title: props.member.userId,
|
title: props.title || props.member.userId,
|
||||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||||
props.width,
|
props.width,
|
||||||
props.height,
|
props.height,
|
||||||
props.resizeMethod)
|
props.resizeMethod)
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
|
|
||||||
var {member, onClick, ...otherProps} = this.props;
|
var {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||||
|
|
||||||
if (this.props.viewUserOnClick) {
|
if (viewUserOnClick) {
|
||||||
onClick = () => {
|
onClick = () => {
|
||||||
dispatcher.dispatch({
|
dispatcher.dispatch({
|
||||||
action: 'view_user',
|
action: 'view_user',
|
||||||
member: this.props.member,
|
member: this.props.member,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -39,7 +39,7 @@ module.exports = React.createClass({
|
||||||
height: 36,
|
height: 36,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
oobData: {},
|
oobData: {},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -51,7 +51,7 @@ module.exports = React.createClass({
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
this.setState({
|
this.setState({
|
||||||
urls: this.getImageUrls(newProps)
|
urls: this.getImageUrls(newProps)
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getImageUrls: function(props) {
|
getImageUrls: function(props) {
|
||||||
|
|
|
@ -40,7 +40,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onValueChanged: function(ev) {
|
onValueChanged: function(ev) {
|
||||||
this.props.onChange(ev.target.value)
|
this.props.onChange(ev.target.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
|
72
src/components/views/dialogs/BaseDialog.js
Normal file
72
src/components/views/dialogs/BaseDialog.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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 * as KeyCode from '../../../KeyCode';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic container for modal dialogs.
|
||||||
|
*
|
||||||
|
* Includes a div for the title, and a keypress handler which cancels the
|
||||||
|
* dialog on escape.
|
||||||
|
*/
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'BaseDialog',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// onFinished callback to call when Escape is pressed
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
// callback to call when Enter is pressed
|
||||||
|
onEnterPressed: React.PropTypes.func,
|
||||||
|
|
||||||
|
// CSS class to apply to dialog div
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
|
||||||
|
// Title for the dialog.
|
||||||
|
// (could probably actually be something more complicated than a string if desired)
|
||||||
|
title: React.PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// children should be the content of the dialog
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
},
|
||||||
|
|
||||||
|
_onKeyDown: function(e) {
|
||||||
|
if (e.keyCode === KeyCode.ESCAPE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onFinished();
|
||||||
|
} else if (e.keyCode === KeyCode.ENTER) {
|
||||||
|
if (this.props.onEnterPressed) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onEnterPressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||||
|
<div className='mx_Dialog_title'>
|
||||||
|
{ this.props.title }
|
||||||
|
</div>
|
||||||
|
{ this.props.children }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -14,19 +14,30 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var classNames = require('classnames');
|
import classNames from 'classnames';
|
||||||
var sdk = require("../../../index");
|
import sdk from '../../../index';
|
||||||
var Invite = require("../../../Invite");
|
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
|
||||||
var createRoom = require("../../../createRoom");
|
import createRoom from '../../../createRoom';
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
var DMRoomMap = require('../../../utils/DMRoomMap');
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
var rate_limited_func = require("../../../ratelimitedfunc");
|
import rate_limited_func from '../../../ratelimitedfunc';
|
||||||
var dis = require("../../../dispatcher");
|
import dis from '../../../dispatcher';
|
||||||
var Modal = require('../../../Modal');
|
import Modal from '../../../Modal';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import q from 'q';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Escapes a string so it can be used in a RegExp
|
||||||
|
* Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ]
|
||||||
|
* From http://stackoverflow.com/a/6969486
|
||||||
|
*/
|
||||||
|
function escapeRegExp(str) {
|
||||||
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: "ChatInviteDialog",
|
displayName: "ChatInviteDialog",
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -48,7 +59,7 @@ module.exports = React.createClass({
|
||||||
title: "Start a chat",
|
title: "Start a chat",
|
||||||
description: "Who would you like to communicate with?",
|
description: "Who would you like to communicate with?",
|
||||||
value: "",
|
value: "",
|
||||||
placeholder: "User ID, Name or email",
|
placeholder: "Email, name or matrix ID",
|
||||||
button: "Start Chat",
|
button: "Start Chat",
|
||||||
focus: true
|
focus: true
|
||||||
};
|
};
|
||||||
|
@ -57,7 +68,14 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
error: false,
|
error: false,
|
||||||
|
|
||||||
|
// List of AddressTile.InviteAddressType objects represeting
|
||||||
|
// the list of addresses we're going to invite
|
||||||
inviteList: [],
|
inviteList: [],
|
||||||
|
|
||||||
|
// List of AddressTile.InviteAddressType objects represeting
|
||||||
|
// the set of autocompletion results for the current search
|
||||||
|
// query.
|
||||||
queryList: [],
|
queryList: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -71,15 +89,12 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onButtonClick: function() {
|
onButtonClick: function() {
|
||||||
var inviteList = this.state.inviteList.slice();
|
let inviteList = this.state.inviteList.slice();
|
||||||
// Check the text input field to see if user has an unconverted address
|
// Check the text input field to see if user has an unconverted address
|
||||||
// If there is and it's valid add it to the local inviteList
|
// If there is and it's valid add it to the local inviteList
|
||||||
var check = Invite.isValidAddress(this.refs.textinput.value);
|
if (this.refs.textinput.value !== '') {
|
||||||
if (check === true || check === null) {
|
inviteList = this._addInputToList();
|
||||||
inviteList.push(this.refs.textinput.value);
|
if (inviteList === null) return;
|
||||||
} else if (this.refs.textinput.value.length > 0) {
|
|
||||||
this.setState({ error: true });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inviteList.length > 0) {
|
if (inviteList.length > 0) {
|
||||||
|
@ -119,15 +134,15 @@ module.exports = React.createClass({
|
||||||
} else if (e.keyCode === 38) { // up arrow
|
} else if (e.keyCode === 38) { // up arrow
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.addressSelector.onKeyUp();
|
this.addressSelector.moveSelectionUp();
|
||||||
} else if (e.keyCode === 40) { // down arrow
|
} else if (e.keyCode === 40) { // down arrow
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.addressSelector.onKeyDown();
|
this.addressSelector.moveSelectionDown();
|
||||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.addressSelector.onKeySelect();
|
this.addressSelector.chooseSelection();
|
||||||
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
|
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -135,33 +150,56 @@ module.exports = React.createClass({
|
||||||
} else if (e.keyCode === 13) { // enter
|
} else if (e.keyCode === 13) { // enter
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onButtonClick();
|
if (this.refs.textinput.value == '') {
|
||||||
|
// if there's nothing in the input box, submit the form
|
||||||
|
this.onButtonClick();
|
||||||
|
} else {
|
||||||
|
this._addInputToList();
|
||||||
|
}
|
||||||
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
|
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var check = Invite.isValidAddress(this.refs.textinput.value);
|
this._addInputToList();
|
||||||
if (check === true || check === null) {
|
|
||||||
var inviteList = this.state.inviteList.slice();
|
|
||||||
inviteList.push(this.refs.textinput.value.trim());
|
|
||||||
this.setState({
|
|
||||||
inviteList: inviteList,
|
|
||||||
queryList: [],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ error: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onQueryChanged: function(ev) {
|
onQueryChanged: function(ev) {
|
||||||
var query = ev.target.value;
|
const query = ev.target.value;
|
||||||
var queryList = [];
|
let queryList = [];
|
||||||
|
|
||||||
// Only do search if there is something to search
|
// Only do search if there is something to search
|
||||||
if (query.length > 0) {
|
if (query.length > 0 && query != '@') {
|
||||||
|
// filter the known users list
|
||||||
queryList = this._userList.filter((user) => {
|
queryList = this._userList.filter((user) => {
|
||||||
return this._matches(query, user);
|
return this._matches(query, user);
|
||||||
|
}).map((user) => {
|
||||||
|
// Return objects, structure of which is defined
|
||||||
|
// by InviteAddressType
|
||||||
|
return {
|
||||||
|
addressType: 'mx',
|
||||||
|
address: user.userId,
|
||||||
|
displayName: user.displayName,
|
||||||
|
avatarMxc: user.avatarUrl,
|
||||||
|
isKnown: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If the query isn't a user we know about, but is a
|
||||||
|
// valid address, add an entry for that
|
||||||
|
if (queryList.length == 0) {
|
||||||
|
const addrType = getAddressType(query);
|
||||||
|
if (addrType !== null) {
|
||||||
|
queryList[0] = {
|
||||||
|
addressType: addrType,
|
||||||
|
address: query,
|
||||||
|
isKnown: false,
|
||||||
|
};
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
|
if (addrType == 'email') {
|
||||||
|
this._lookupThreepid(addrType, query).done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -179,7 +217,8 @@ module.exports = React.createClass({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
}
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(index) {
|
onClick: function(index) {
|
||||||
|
@ -191,11 +230,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onSelected: function(index) {
|
onSelected: function(index) {
|
||||||
var inviteList = this.state.inviteList.slice();
|
var inviteList = this.state.inviteList.slice();
|
||||||
inviteList.push(this.state.queryList[index].userId);
|
inviteList.push(this.state.queryList[index]);
|
||||||
this.setState({
|
this.setState({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getDirectMessageRoom: function(addr) {
|
_getDirectMessageRoom: function(addr) {
|
||||||
|
@ -226,10 +266,14 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addrTexts = addrs.map((addr) => {
|
||||||
|
return addr.address;
|
||||||
|
});
|
||||||
|
|
||||||
if (this.props.roomId) {
|
if (this.props.roomId) {
|
||||||
// Invite new user to a room
|
// Invite new user to a room
|
||||||
var self = this;
|
var self = this;
|
||||||
Invite.inviteMultipleToRoom(this.props.roomId, addrs)
|
inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||||
.then(function(addrs) {
|
.then(function(addrs) {
|
||||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||||
return self._showAnyInviteErrors(addrs, room);
|
return self._showAnyInviteErrors(addrs, room);
|
||||||
|
@ -244,9 +288,9 @@ module.exports = React.createClass({
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.done();
|
.done();
|
||||||
} else if (this._isDmChat(addrs)) {
|
} else if (this._isDmChat(addrTexts)) {
|
||||||
// Start the DM chat
|
// Start the DM chat
|
||||||
createRoom({dmUserId: addrs[0]})
|
createRoom({dmUserId: addrTexts[0]})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -263,7 +307,7 @@ module.exports = React.createClass({
|
||||||
var room;
|
var room;
|
||||||
createRoom().then(function(roomId) {
|
createRoom().then(function(roomId) {
|
||||||
room = MatrixClientPeg.get().getRoom(roomId);
|
room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
return Invite.inviteMultipleToRoom(roomId, addrs);
|
return inviteMultipleToRoom(roomId, addrTexts);
|
||||||
})
|
})
|
||||||
.then(function(addrs) {
|
.then(function(addrs) {
|
||||||
return self._showAnyInviteErrors(addrs, room);
|
return self._showAnyInviteErrors(addrs, room);
|
||||||
|
@ -281,7 +325,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close - this will happen before the above, as that is async
|
// Close - this will happen before the above, as that is async
|
||||||
this.props.onFinished(true, addrs);
|
this.props.onFinished(true, addrTexts);
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateUserList: new rate_limited_func(function() {
|
_updateUserList: new rate_limited_func(function() {
|
||||||
|
@ -315,19 +359,27 @@ module.exports = React.createClass({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// split spaces in name and try matching constituent parts
|
// Try to find the query following a "word boundary", except that
|
||||||
var parts = name.split(" ");
|
// this does avoids using \b because it only considers letters from
|
||||||
for (var i = 0; i < parts.length; i++) {
|
// the roman alphabet to be word characters.
|
||||||
if (parts[i].indexOf(query) === 0) {
|
// Instead, we look for the query following either:
|
||||||
return true;
|
// * The start of the string
|
||||||
}
|
// * Whitespace, or
|
||||||
|
// * A fixed number of punctuation characters
|
||||||
|
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
|
||||||
|
if (expr.test(name)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
_isOnInviteList: function(uid) {
|
_isOnInviteList: function(uid) {
|
||||||
for (let i = 0; i < this.state.inviteList.length; i++) {
|
for (let i = 0; i < this.state.inviteList.length; i++) {
|
||||||
if (this.state.inviteList[i].toLowerCase() === uid) {
|
if (
|
||||||
|
this.state.inviteList[i].addressType == 'mx' &&
|
||||||
|
this.state.inviteList[i].address.toLowerCase() === uid
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,7 +387,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_isDmChat: function(addrs) {
|
_isDmChat: function(addrs) {
|
||||||
if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
@ -361,9 +413,74 @@ module.exports = React.createClass({
|
||||||
return addrs;
|
return addrs;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_addInputToList: function() {
|
||||||
|
const addressText = this.refs.textinput.value.trim();
|
||||||
|
const addrType = getAddressType(addressText);
|
||||||
|
const addrObj = {
|
||||||
|
addressType: addrType,
|
||||||
|
address: addressText,
|
||||||
|
isKnown: false,
|
||||||
|
};
|
||||||
|
if (addrType == null) {
|
||||||
|
this.setState({ error: true });
|
||||||
|
return null;
|
||||||
|
} else if (addrType == 'mx') {
|
||||||
|
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||||
|
if (user) {
|
||||||
|
addrObj.displayName = user.displayName;
|
||||||
|
addrObj.avatarMxc = user.avatarUrl;
|
||||||
|
addrObj.isKnown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteList = this.state.inviteList.slice();
|
||||||
|
inviteList.push(addrObj);
|
||||||
|
this.setState({
|
||||||
|
inviteList: inviteList,
|
||||||
|
queryList: [],
|
||||||
|
});
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
|
return inviteList;
|
||||||
|
},
|
||||||
|
|
||||||
|
_lookupThreepid: function(medium, address) {
|
||||||
|
let cancelled = false;
|
||||||
|
// Note that we can't safely remove this after we're done
|
||||||
|
// because we don't know that it's the same one, so we just
|
||||||
|
// leave it: it's replacing the old one each time so it's
|
||||||
|
// not like they leak.
|
||||||
|
this._cancelThreepidLookup = function() {
|
||||||
|
cancelled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait a bit to let the user finish typing
|
||||||
|
return q.delay(500).then(() => {
|
||||||
|
if (cancelled) return null;
|
||||||
|
return MatrixClientPeg.get().lookupThreePid(medium, address);
|
||||||
|
}).then((res) => {
|
||||||
|
if (res === null || !res.mxid) return null;
|
||||||
|
if (cancelled) return null;
|
||||||
|
|
||||||
|
return MatrixClientPeg.get().getProfileInfo(res.mxid);
|
||||||
|
}).then((res) => {
|
||||||
|
if (res === null) return null;
|
||||||
|
if (cancelled) return null;
|
||||||
|
this.setState({
|
||||||
|
queryList: [{
|
||||||
|
// an InviteAddressType
|
||||||
|
addressType: medium,
|
||||||
|
address: address,
|
||||||
|
displayName: res.displayname,
|
||||||
|
avatarMxc: res.avatar_url,
|
||||||
|
isKnown: true,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
var AddressSelector = sdk.getComponent("elements.AddressSelector");
|
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||||
this.scrollElement = null;
|
this.scrollElement = null;
|
||||||
|
|
||||||
var query = [];
|
var query = [];
|
||||||
|
@ -394,13 +511,18 @@ module.exports = React.createClass({
|
||||||
var error;
|
var error;
|
||||||
var addressSelector;
|
var addressSelector;
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>
|
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>;
|
||||||
} else {
|
} else {
|
||||||
|
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
|
||||||
|
Searching known users
|
||||||
|
</div>;
|
||||||
addressSelector = (
|
addressSelector = (
|
||||||
<AddressSelector ref={(ref) => {this.addressSelector = ref}}
|
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||||
addressList={ this.state.queryList }
|
addressList={ this.state.queryList }
|
||||||
onSelected={ this.onSelected }
|
onSelected={ this.onSelected }
|
||||||
truncateAt={ TRUNCATE_QUERY_LIST } />
|
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||||
|
header={ addressSelectorHeader }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,9 +531,10 @@ module.exports = React.createClass({
|
||||||
<div className="mx_Dialog_title">
|
<div className="mx_Dialog_title">
|
||||||
{this.props.title}
|
{this.props.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ChatInviteDialog_cancel" onClick={this.onCancel} >
|
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||||
|
onClick={this.onCancel} >
|
||||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||||
</div>
|
</AccessibleButton>
|
||||||
<div className="mx_ChatInviteDialog_label">
|
<div className="mx_ChatInviteDialog_label">
|
||||||
<label htmlFor="textinput">{ this.props.description }</label>
|
<label htmlFor="textinput">{ this.props.description }</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -80,8 +80,8 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
let error = null;
|
let error = null;
|
||||||
if (this.state.errStr) {
|
if (this.state.errStr) {
|
||||||
error = <div className="error">
|
error = <div className="error">
|
||||||
{this.state.err_str}
|
{this.state.errStr}
|
||||||
</div>
|
</div>;
|
||||||
passwordBoxClass = 'error';
|
passwordBoxClass = 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
if (!this.state.busy) {
|
if (!this.state.busy) {
|
||||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -25,9 +25,10 @@ limitations under the License.
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ErrorDialog',
|
displayName: 'ErrorDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
|
@ -49,20 +50,11 @@ module.exports = React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
|
||||||
if (e.keyCode === 27) { // escape
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_ErrorDialog" onKeyDown={ this.onKeyDown }>
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
<div className="mx_Dialog_title">
|
title={this.props.title}>
|
||||||
{this.props.title}
|
|
||||||
</div>
|
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,7 +63,7 @@ module.exports = React.createClass({
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -111,20 +111,9 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onKeyDown: function(e) {
|
_onEnterPressed: function(e) {
|
||||||
if (e.keyCode === 27) { // escape
|
if (this.state.submitButtonEnabled && !this.state.busy) {
|
||||||
e.stopPropagation();
|
this._onSubmit();
|
||||||
e.preventDefault();
|
|
||||||
if (!this.state.busy) {
|
|
||||||
this._onCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (e.keyCode === 13) { // enter
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.state.submitButtonEnabled && !this.state.busy) {
|
|
||||||
this._onSubmit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -171,6 +160,7 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
if (this.state.errorText) {
|
if (this.state.errorText) {
|
||||||
|
@ -200,10 +190,11 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_InteractiveAuthDialog" onKeyDown={this._onKeyDown}>
|
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||||
<div className="mx_Dialog_title">
|
onEnterPressed={this._onEnterPressed}
|
||||||
{this.props.title}
|
onFinished={this.props.onFinished}
|
||||||
</div>
|
title={this.props.title}
|
||||||
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>This operation requires additional authentication.</p>
|
<p>This operation requires additional authentication.</p>
|
||||||
{this._renderCurrentStage()}
|
{this._renderCurrentStage()}
|
||||||
|
@ -213,7 +204,7 @@ export default React.createClass({
|
||||||
{submitButton}
|
{submitButton}
|
||||||
{cancelButton}
|
{cancelButton}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
var React = require('react');
|
|
||||||
var dis = require("../../../dispatcher");
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
displayName: 'LogoutPrompt',
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
onFinished: React.PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
logOut: function() {
|
|
||||||
dis.dispatch({action: 'logout'});
|
|
||||||
if (this.props.onFinished) {
|
|
||||||
this.props.onFinished();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelPrompt: function() {
|
|
||||||
if (this.props.onFinished) {
|
|
||||||
this.props.onFinished();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
|
||||||
if (e.keyCode === 27) { // escape
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.cancelPrompt();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mx_Dialog_content">
|
|
||||||
Sign out?
|
|
||||||
</div>
|
|
||||||
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
|
|
||||||
<button className="mx_Dialog_primary" autoFocus onClick={this.logOut}>Sign Out</button>
|
|
||||||
<button onClick={this.cancelPrompt}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -23,8 +23,9 @@ limitations under the License.
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var dis = require("../../../dispatcher");
|
import dis from '../../../dispatcher';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'NeedToRegisterDialog',
|
displayName: 'NeedToRegisterDialog',
|
||||||
|
@ -54,11 +55,12 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_NeedToRegisterDialog">
|
<BaseDialog className="mx_NeedToRegisterDialog"
|
||||||
<div className="mx_Dialog_title">
|
onFinished={this.props.onFinished}
|
||||||
{this.props.title}
|
title={this.props.title}
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +72,7 @@ module.exports = React.createClass({
|
||||||
Register
|
Register
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'QuestionDialog',
|
displayName: 'QuestionDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
|
@ -46,25 +47,13 @@ module.exports = React.createClass({
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
|
||||||
if (e.keyCode === 27) { // escape
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(false);
|
|
||||||
}
|
|
||||||
else if (e.keyCode === 13) { // enter
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_QuestionDialog" onKeyDown={ this.onKeyDown }>
|
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||||
<div className="mx_Dialog_title">
|
onEnterPressed={ this.onOk }
|
||||||
{this.props.title}
|
title={this.props.title}
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,7 +66,7 @@ module.exports = React.createClass({
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var sdk = require("../../../index.js");
|
import sdk from '../../../index';
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
/**
|
||||||
|
* Prompt the user to set a display name.
|
||||||
|
*
|
||||||
|
* On success, `onFinished(true, newDisplayName)` is called.
|
||||||
|
*/
|
||||||
|
export default React.createClass({
|
||||||
displayName: 'SetDisplayNameDialog',
|
displayName: 'SetDisplayNameDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
@ -42,10 +47,6 @@ module.exports = React.createClass({
|
||||||
this.refs.input_value.select();
|
this.refs.input_value.select();
|
||||||
},
|
},
|
||||||
|
|
||||||
getValue: function() {
|
|
||||||
return this.state.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
onValueChange: function(ev) {
|
onValueChange: function(ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
value: ev.target.value
|
value: ev.target.value
|
||||||
|
@ -54,16 +55,17 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onFormSubmit: function(ev) {
|
onFormSubmit: function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true, this.state.value);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_SetDisplayNameDialog">
|
<BaseDialog className="mx_SetDisplayNameDialog"
|
||||||
<div className="mx_Dialog_title">
|
onFinished={this.props.onFinished}
|
||||||
Set a Display Name
|
title="Set a Display Name"
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
||||||
What would you like it to be?
|
What would you like it to be?
|
||||||
|
@ -79,7 +81,7 @@ module.exports = React.createClass({
|
||||||
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'TextInputDialog',
|
displayName: 'TextInputDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
|
@ -27,7 +28,7 @@ module.exports = React.createClass({
|
||||||
value: React.PropTypes.string,
|
value: React.PropTypes.string,
|
||||||
button: React.PropTypes.string,
|
button: React.PropTypes.string,
|
||||||
focus: React.PropTypes.bool,
|
focus: React.PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -36,7 +37,7 @@ module.exports = React.createClass({
|
||||||
value: "",
|
value: "",
|
||||||
description: "",
|
description: "",
|
||||||
button: "OK",
|
button: "OK",
|
||||||
focus: true
|
focus: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -55,25 +56,13 @@ module.exports = React.createClass({
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
|
||||||
if (e.keyCode === 27) { // escape
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(false);
|
|
||||||
}
|
|
||||||
else if (e.keyCode === 13) { // enter
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(true, this.refs.textinput.value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_TextInputDialog">
|
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||||
<div className="mx_Dialog_title">
|
onEnterPressed={this.onOk}
|
||||||
{this.props.title}
|
title={this.props.title}
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_TextInputDialog_label">
|
<div className="mx_TextInputDialog_label">
|
||||||
<label htmlFor="textinput"> {this.props.description} </label>
|
<label htmlFor="textinput"> {this.props.description} </label>
|
||||||
|
@ -90,7 +79,7 @@ module.exports = React.createClass({
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
54
src/components/views/elements/AccessibleButton.js
Normal file
54
src/components/views/elements/AccessibleButton.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 Jani Mustonen
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||||
|
* as a button. Identifies the element as a button, setting proper tab
|
||||||
|
* indexing and keyboard activation behavior.
|
||||||
|
*
|
||||||
|
* @param {Object} props react element properties
|
||||||
|
* @returns {Object} rendered react
|
||||||
|
*/
|
||||||
|
export default function AccessibleButton(props) {
|
||||||
|
const {element, onClick, children, ...restProps} = props;
|
||||||
|
restProps.onClick = onClick;
|
||||||
|
restProps.onKeyDown = function(e) {
|
||||||
|
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
|
||||||
|
};
|
||||||
|
restProps.tabIndex = restProps.tabIndex || "0";
|
||||||
|
restProps.role = "button";
|
||||||
|
return React.createElement(element, restProps, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* children: React's magic prop. Represents all children given to the element.
|
||||||
|
* element: (optional) The base element type. "div" by default.
|
||||||
|
* onClick: (required) Event handler for button activation. Should be
|
||||||
|
* implemented exactly like a normal onClick handler.
|
||||||
|
*/
|
||||||
|
AccessibleButton.propTypes = {
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
element: React.PropTypes.string,
|
||||||
|
onClick: React.PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
AccessibleButton.defaultProps = {
|
||||||
|
element: 'div',
|
||||||
|
};
|
||||||
|
|
||||||
|
AccessibleButton.displayName = "AccessibleButton";
|
|
@ -16,18 +16,24 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var sdk = require("../../../index");
|
import sdk from '../../../index';
|
||||||
var classNames = require('classnames');
|
import classNames from 'classnames';
|
||||||
|
import { InviteAddressType } from './AddressTile';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'AddressSelector',
|
displayName: 'AddressSelector',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onSelected: React.PropTypes.func.isRequired,
|
onSelected: React.PropTypes.func.isRequired,
|
||||||
addressList: React.PropTypes.array.isRequired,
|
|
||||||
|
// List of the addresses to display
|
||||||
|
addressList: React.PropTypes.arrayOf(InviteAddressType).isRequired,
|
||||||
truncateAt: React.PropTypes.number.isRequired,
|
truncateAt: React.PropTypes.number.isRequired,
|
||||||
selected: React.PropTypes.number,
|
selected: React.PropTypes.number,
|
||||||
|
|
||||||
|
// Element to put as a header on top of the list
|
||||||
|
header: React.PropTypes.node,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -55,7 +61,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyUp: function() {
|
moveSelectionUp: function() {
|
||||||
if (this.state.selected > 0) {
|
if (this.state.selected > 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selected: this.state.selected - 1,
|
selected: this.state.selected - 1,
|
||||||
|
@ -64,7 +70,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function() {
|
moveSelectionDown: function() {
|
||||||
if (this.state.selected < this._maxSelected(this.props.addressList)) {
|
if (this.state.selected < this._maxSelected(this.props.addressList)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selected: this.state.selected + 1,
|
selected: this.state.selected + 1,
|
||||||
|
@ -73,25 +79,19 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeySelect: function() {
|
chooseSelection: function() {
|
||||||
this.selectAddress(this.state.selected);
|
this.selectAddress(this.state.selected);
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(index) {
|
onClick: function(index) {
|
||||||
var self = this;
|
this.selectAddress(index);
|
||||||
return function() {
|
|
||||||
self.selectAddress(index);
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseEnter: function(index) {
|
onMouseEnter: function(index) {
|
||||||
var self = this;
|
this.setState({
|
||||||
return function() {
|
selected: index,
|
||||||
self.setState({
|
hover: true,
|
||||||
selected: index,
|
});
|
||||||
hover: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseLeave: function() {
|
onMouseLeave: function() {
|
||||||
|
@ -124,8 +124,8 @@ module.exports = React.createClass({
|
||||||
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
|
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
|
||||||
// method, how far to scroll when using the arrow keys
|
// method, how far to scroll when using the arrow keys
|
||||||
addressList.push(
|
addressList.push(
|
||||||
<div className={classes} onClick={this.onClick(i)} onMouseEnter={this.onMouseEnter(i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
|
<div className={classes} onClick={this.onClick.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
|
||||||
<AddressTile address={this.props.addressList[i].userId} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_maxSelected: function(list) {
|
_maxSelected: function(list) {
|
||||||
var listSize = list.length === 0 ? 0 : list.length - 1;
|
var listSize = list.length === 0 ? 0 : list.length - 1;
|
||||||
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize
|
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
|
||||||
return maxSelected;
|
return maxSelected;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -146,7 +146,8 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes} ref={(ref) => {this.scrollElement = ref}}>
|
<div className={classes} ref={(ref) => {this.scrollElement = ref;}}>
|
||||||
|
{ this.props.header }
|
||||||
{ this.createAddressListTiles() }
|
{ this.createAddressListTiles() }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,16 +23,33 @@ var Invite = require("../../../Invite");
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var Avatar = require('../../../Avatar');
|
var Avatar = require('../../../Avatar');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
// React PropType definition for an object describing
|
||||||
|
// an address that can be invited to a room (which
|
||||||
|
// could be a third party identifier or a matrix ID)
|
||||||
|
// along with some additional information about the
|
||||||
|
// address / target.
|
||||||
|
export const InviteAddressType = React.PropTypes.shape({
|
||||||
|
addressType: React.PropTypes.oneOf([
|
||||||
|
'mx', 'email'
|
||||||
|
]).isRequired,
|
||||||
|
address: React.PropTypes.string.isRequired,
|
||||||
|
displayName: React.PropTypes.string,
|
||||||
|
avatarMxc: React.PropTypes.string,
|
||||||
|
// true if the address is known to be a valid address (eg. is a real
|
||||||
|
// user we've seen) or false otherwise (eg. is just an address the
|
||||||
|
// user has entered)
|
||||||
|
isKnown: React.PropTypes.bool,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
displayName: 'AddressTile',
|
displayName: 'AddressTile',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
address: React.PropTypes.string.isRequired,
|
address: InviteAddressType.isRequired,
|
||||||
canDismiss: React.PropTypes.bool,
|
canDismiss: React.PropTypes.bool,
|
||||||
onDismissed: React.PropTypes.func,
|
onDismissed: React.PropTypes.func,
|
||||||
justified: React.PropTypes.bool,
|
justified: React.PropTypes.bool,
|
||||||
networkName: React.PropTypes.string,
|
|
||||||
networkUrl: React.PropTypes.string,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -40,37 +57,30 @@ module.exports = React.createClass({
|
||||||
canDismiss: false,
|
canDismiss: false,
|
||||||
onDismissed: function() {}, // NOP
|
onDismissed: function() {}, // NOP
|
||||||
justified: false,
|
justified: false,
|
||||||
networkName: "",
|
|
||||||
networkUrl: "",
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var userId, name, imgUrl, email;
|
const address = this.props.address;
|
||||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const name = address.displayName || address.address;
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
|
||||||
|
|
||||||
// Check if the addr is a valid type
|
let imgUrl;
|
||||||
var addrType = Invite.getAddressType(this.props.address);
|
if (address.avatarMxc) {
|
||||||
if (addrType === "mx") {
|
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
let user = MatrixClientPeg.get().getUser(this.props.address);
|
address.avatarMxc, 25, 25, 'crop'
|
||||||
if (user) {
|
);
|
||||||
userId = user.userId;
|
|
||||||
name = user.rawDisplayName || userId;
|
|
||||||
imgUrl = Avatar.avatarUrlForUser(user, 25, 25, "crop");
|
|
||||||
} else {
|
|
||||||
name=this.props.address;
|
|
||||||
imgUrl = "img/icon-mx-user.svg";
|
|
||||||
}
|
|
||||||
} else if (addrType === "email") {
|
|
||||||
email = this.props.address;
|
|
||||||
name="email";
|
|
||||||
imgUrl = "img/icon-email-user.svg";
|
|
||||||
} else {
|
|
||||||
name="Unknown";
|
|
||||||
imgUrl = "img/avatar-error.svg";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (address.addressType === "mx") {
|
||||||
|
if (!imgUrl) imgUrl = 'img/icon-mx-user.svg';
|
||||||
|
} else if (address.addressType === 'email') {
|
||||||
|
if (!imgUrl) imgUrl = 'img/icon-email-user.svg';
|
||||||
|
} else {
|
||||||
|
if (!imgUrl) imgUrl = "img/avatar-error.svg";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing networks for now as they're not really supported
|
||||||
|
/*
|
||||||
var network;
|
var network;
|
||||||
if (this.props.networkUrl !== "") {
|
if (this.props.networkUrl !== "") {
|
||||||
network = (
|
network = (
|
||||||
|
@ -79,16 +89,20 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
var info;
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
var error = false;
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
if (addrType === "mx" && userId) {
|
|
||||||
var nameClasses = classNames({
|
|
||||||
"mx_AddressTile_name": true,
|
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
|
||||||
});
|
|
||||||
|
|
||||||
var idClasses = classNames({
|
const nameClasses = classNames({
|
||||||
|
"mx_AddressTile_name": true,
|
||||||
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
});
|
||||||
|
|
||||||
|
let info;
|
||||||
|
let error = false;
|
||||||
|
if (address.addressType === "mx" && address.isKnown) {
|
||||||
|
const idClasses = classNames({
|
||||||
"mx_AddressTile_id": true,
|
"mx_AddressTile_id": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
@ -96,26 +110,34 @@ module.exports = React.createClass({
|
||||||
info = (
|
info = (
|
||||||
<div className="mx_AddressTile_mx">
|
<div className="mx_AddressTile_mx">
|
||||||
<div className={nameClasses}>{ name }</div>
|
<div className={nameClasses}>{ name }</div>
|
||||||
<div className={idClasses}>{ userId }</div>
|
<div className={idClasses}>{ address.address }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (addrType === "mx") {
|
} else if (address.addressType === "mx") {
|
||||||
var unknownMxClasses = classNames({
|
const unknownMxClasses = classNames({
|
||||||
"mx_AddressTile_unknownMx": true,
|
"mx_AddressTile_unknownMx": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className={unknownMxClasses}>{ this.props.address }</div>
|
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
||||||
);
|
);
|
||||||
} else if (email) {
|
} else if (address.addressType === "email") {
|
||||||
var emailClasses = classNames({
|
const emailClasses = classNames({
|
||||||
"mx_AddressTile_email": true,
|
"mx_AddressTile_email": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let nameNode = null;
|
||||||
|
if (address.displayName) {
|
||||||
|
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
||||||
|
}
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className={emailClasses}>{ email }</div>
|
<div className="mx_AddressTile_mx">
|
||||||
|
<div className={emailClasses}>{ address.address }</div>
|
||||||
|
{nameNode}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
error = true;
|
error = true;
|
||||||
|
@ -129,12 +151,12 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var classes = classNames({
|
const classes = classNames({
|
||||||
"mx_AddressTile": true,
|
"mx_AddressTile": true,
|
||||||
"mx_AddressTile_error": error,
|
"mx_AddressTile_error": error,
|
||||||
});
|
});
|
||||||
|
|
||||||
var dismiss;
|
let dismiss;
|
||||||
if (this.props.canDismiss) {
|
if (this.props.canDismiss) {
|
||||||
dismiss = (
|
dismiss = (
|
||||||
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
|
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
|
||||||
|
@ -145,7 +167,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{ network }
|
|
||||||
<div className="mx_AddressTile_avatar">
|
<div className="mx_AddressTile_avatar">
|
||||||
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,14 +42,14 @@ export default React.createClass({
|
||||||
<div className="mx_UserSettings_cryptoSection">
|
<div className="mx_UserSettings_cryptoSection">
|
||||||
<ul>
|
<ul>
|
||||||
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
||||||
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
|
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
|
||||||
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
|
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
If it matches, press the verify button below.
|
If it matches, press the verify button below.
|
||||||
If it doesnt, then someone else is intercepting this device
|
If it doesnt, then someone else is intercepting this device
|
||||||
and you probably want to press the block button instead.
|
and you probably want to press the blacklist button instead.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In future this verification process will be more sophisticated.
|
In future this verification process will be more sophisticated.
|
||||||
|
@ -73,33 +73,33 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlockClick: function() {
|
onBlacklistClick: function() {
|
||||||
MatrixClientPeg.get().setDeviceBlocked(
|
MatrixClientPeg.get().setDeviceBlocked(
|
||||||
this.props.userId, this.props.device.deviceId, true
|
this.props.userId, this.props.device.deviceId, true
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnblockClick: function() {
|
onUnblacklistClick: function() {
|
||||||
MatrixClientPeg.get().setDeviceBlocked(
|
MatrixClientPeg.get().setDeviceBlocked(
|
||||||
this.props.userId, this.props.device.deviceId, false
|
this.props.userId, this.props.device.deviceId, false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var blockButton = null, verifyButton = null;
|
var blacklistButton = null, verifyButton = null;
|
||||||
|
|
||||||
if (this.props.device.isBlocked()) {
|
if (this.props.device.isBlocked()) {
|
||||||
blockButton = (
|
blacklistButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||||
onClick={this.onUnblockClick}>
|
onClick={this.onUnblacklistClick}>
|
||||||
Unblock
|
Unblacklist
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
blockButton = (
|
blacklistButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
|
||||||
onClick={this.onBlockClick}>
|
onClick={this.onBlacklistClick}>
|
||||||
Block
|
Blacklist
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ export default React.createClass({
|
||||||
verifyButton = (
|
verifyButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||||
onClick={this.onVerifyClick}>
|
onClick={this.onVerifyClick}>
|
||||||
Verify
|
Verify...
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberDeviceInfo mx_DeviceVerifyButtons" >
|
<div className="mx_MemberDeviceInfo mx_DeviceVerifyButtons" >
|
||||||
{ verifyButton }
|
{ verifyButton }
|
||||||
{ blockButton }
|
{ blacklistButton }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -89,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
|
|
||||||
return <span className={classnames(searchbox_classes)}>
|
return <span className={classnames(searchbox_classes)}>
|
||||||
<div className="mx_DirectorySearchBox_container">
|
<div className="mx_DirectorySearchBox_container">
|
||||||
<input type="text" value={this.state.value}
|
<input type="text" name="dirsearch" value={this.state.value}
|
||||||
className="mx_DirectorySearchBox_input"
|
className="mx_DirectorySearchBox_input"
|
||||||
ref={this._collectInput}
|
ref={this._collectInput}
|
||||||
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
||||||
|
|
|
@ -57,7 +57,7 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
phase: this.Phases.Display,
|
phase: this.Phases.Display,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
@ -164,7 +164,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: this.Phases.Edit,
|
phase: this.Phases.Edit,
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onFocus: function(ev) {
|
onFocus: function(ev) {
|
||||||
|
@ -197,9 +197,9 @@ module.exports = React.createClass({
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
|
|
||||||
if (this.props.blurToCancel)
|
if (this.props.blurToCancel)
|
||||||
this.cancelEdit();
|
{this.cancelEdit();}
|
||||||
else
|
else
|
||||||
this.onFinish(ev);
|
{this.onFinish(ev);}
|
||||||
|
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
||||||
events: React.PropTypes.array.isRequired,
|
events: React.PropTypes.array.isRequired,
|
||||||
// An array of EventTiles to render when expanded
|
// An array of EventTiles to render when expanded
|
||||||
children: React.PropTypes.array.isRequired,
|
children: React.PropTypes.array.isRequired,
|
||||||
// The maximum number of names to show in either the join or leave summaries
|
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||||
summaryLength: React.PropTypes.number,
|
summaryLength: React.PropTypes.number,
|
||||||
// The maximum number of avatars to display in the summary
|
// The maximum number of avatars to display in the summary
|
||||||
avatarsMaxLength: React.PropTypes.number,
|
avatarsMaxLength: React.PropTypes.number,
|
||||||
|
@ -40,93 +40,229 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
summaryLength: 3,
|
summaryLength: 1,
|
||||||
threshold: 3,
|
threshold: 3,
|
||||||
avatarsMaxLength: 5
|
avatarsMaxLength: 5,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
|
// Update if
|
||||||
|
// - The number of summarised events has changed
|
||||||
|
// - or if the summary is currently expanded
|
||||||
|
// - or if the summary is about to toggle to become collapsed
|
||||||
|
// - or if there are fewEvents, meaning the child eventTiles are shown as-is
|
||||||
|
return (
|
||||||
|
nextProps.events.length !== this.props.events.length ||
|
||||||
|
this.state.expanded || nextState.expanded ||
|
||||||
|
nextProps.events.length < this.props.threshold
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
_toggleSummary: function() {
|
_toggleSummary: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: !this.state.expanded,
|
expanded: !this.state.expanded,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_getEventSenderName: function(ev) {
|
/**
|
||||||
if (!ev) {
|
* Render the JSX for users aggregated by their transition sequences (`eventAggregates`) where
|
||||||
return 'undefined';
|
* the sequences are ordered by `orderedTransitionSequences`.
|
||||||
}
|
* @param {object[]} eventAggregates a map of transition sequence to array of user display names
|
||||||
return ev.sender.name || ev.event.content.displayname || ev.getSender();
|
* or user IDs.
|
||||||
},
|
* @param {string[]} orderedTransitionSequences an array which is some ordering of
|
||||||
|
* `Object.keys(eventAggregates)`.
|
||||||
|
* @returns {ReactElement} a single <span> containing the textual summary of the aggregated
|
||||||
|
* events that occurred.
|
||||||
|
*/
|
||||||
|
_renderSummary: function(eventAggregates, orderedTransitionSequences) {
|
||||||
|
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||||
|
const userNames = eventAggregates[transitions];
|
||||||
|
const nameList = this._renderNameList(userNames);
|
||||||
|
const plural = userNames.length > 1;
|
||||||
|
|
||||||
_renderNameList: function(events) {
|
const splitTransitions = transitions.split(',');
|
||||||
if (events.length === 0) {
|
|
||||||
|
// Some neighbouring transitions are common, so canonicalise some into "pair"
|
||||||
|
// transitions
|
||||||
|
const canonicalTransitions = this._getCanonicalTransitions(splitTransitions);
|
||||||
|
// Transform into consecutive repetitions of the same transition (like 5
|
||||||
|
// consecutive 'joined_and_left's)
|
||||||
|
const coalescedTransitions = this._coalesceRepeatedTransitions(
|
||||||
|
canonicalTransitions
|
||||||
|
);
|
||||||
|
|
||||||
|
const descs = coalescedTransitions.map((t) => {
|
||||||
|
return this._getDescriptionForTransition(
|
||||||
|
t.transitionType, plural, t.repeats
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const desc = this._renderCommaSeparatedList(descs);
|
||||||
|
|
||||||
|
return nameList + " " + desc;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!summaries) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let originalNumber = events.length;
|
|
||||||
events = events.slice(0, this.props.summaryLength);
|
|
||||||
let lastEvent = events.pop();
|
|
||||||
|
|
||||||
let names = events.map((ev) => {
|
|
||||||
return this._getEventSenderName(ev);
|
|
||||||
}).join(', ');
|
|
||||||
|
|
||||||
let lastName = this._getEventSenderName(lastEvent);
|
|
||||||
if (names.length === 0) {
|
|
||||||
// special-case for a single event
|
|
||||||
return lastName;
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining = originalNumber - this.props.summaryLength;
|
|
||||||
if (remaining > 0) {
|
|
||||||
// name1, name2, name3, and 100 others
|
|
||||||
return names + ', ' + lastName + ', and ' + remaining + ' others';
|
|
||||||
} else {
|
|
||||||
// name1, name2 and name3
|
|
||||||
return names + ' and ' + lastName;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderSummary: function(joinEvents, leaveEvents) {
|
|
||||||
let joiners = this._renderNameList(joinEvents);
|
|
||||||
let leavers = this._renderNameList(leaveEvents);
|
|
||||||
|
|
||||||
let joinSummary = null;
|
|
||||||
if (joiners) {
|
|
||||||
joinSummary = (
|
|
||||||
<span>
|
|
||||||
{joiners} joined the room
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let leaveSummary = null;
|
|
||||||
if (leavers) {
|
|
||||||
leaveSummary = (
|
|
||||||
<span>
|
|
||||||
{leavers} left the room
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{joinSummary}{joinSummary && leaveSummary?'; ':''}
|
{summaries.join(", ")}
|
||||||
{leaveSummary}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderAvatars: function(events) {
|
/**
|
||||||
let avatars = events.slice(0, this.props.avatarsMaxLength).map((e) => {
|
* @param {string[]} users an array of user display names or user IDs.
|
||||||
|
* @returns {string} a comma-separated list that ends with "and [n] others" if there are
|
||||||
|
* more items in `users` than `this.props.summaryLength`, which is the number of names
|
||||||
|
* included before "and [n] others".
|
||||||
|
*/
|
||||||
|
_renderNameList: function(users) {
|
||||||
|
return this._renderCommaSeparatedList(users, this.props.summaryLength);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonicalise an array of transitions such that some pairs of transitions become
|
||||||
|
* single transitions. For example an input ['joined','left'] would result in an output
|
||||||
|
* ['joined_and_left'].
|
||||||
|
* @param {string[]} transitions an array of transitions.
|
||||||
|
* @returns {string[]} an array of transitions.
|
||||||
|
*/
|
||||||
|
_getCanonicalTransitions: function(transitions) {
|
||||||
|
const modMap = {
|
||||||
|
'joined': {
|
||||||
|
'after': 'left',
|
||||||
|
'newTransition': 'joined_and_left',
|
||||||
|
},
|
||||||
|
'left': {
|
||||||
|
'after': 'joined',
|
||||||
|
'newTransition': 'left_and_joined',
|
||||||
|
},
|
||||||
|
// $currentTransition : {
|
||||||
|
// 'after' : $nextTransition,
|
||||||
|
// 'newTransition' : 'new_transition_type',
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
const res = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < transitions.length; i++) {
|
||||||
|
const t = transitions[i];
|
||||||
|
const t2 = transitions[i + 1];
|
||||||
|
|
||||||
|
let transition = t;
|
||||||
|
|
||||||
|
if (i < transitions.length - 1 && modMap[t] && modMap[t].after === t2) {
|
||||||
|
transition = modMap[t].newTransition;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push(transition);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform an array of transitions into an array of transitions and how many times
|
||||||
|
* they are repeated consecutively.
|
||||||
|
*
|
||||||
|
* An array of 123 "joined_and_left" transitions, would result in:
|
||||||
|
* ```
|
||||||
|
* [{
|
||||||
|
* transitionType: "joined_and_left"
|
||||||
|
* repeats: 123
|
||||||
|
* }]
|
||||||
|
* ```
|
||||||
|
* @param {string[]} transitions the array of transitions to transform.
|
||||||
|
* @returns {object[]} an array of coalesced transitions.
|
||||||
|
*/
|
||||||
|
_coalesceRepeatedTransitions: function(transitions) {
|
||||||
|
const res = [];
|
||||||
|
for (let i = 0; i < transitions.length; i++) {
|
||||||
|
if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
|
||||||
|
res[res.length - 1].repeats += 1;
|
||||||
|
} else {
|
||||||
|
res.push({
|
||||||
|
transitionType: transitions[i],
|
||||||
|
repeats: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a certain transition, t, describe what happened to the users that
|
||||||
|
* underwent the transition.
|
||||||
|
* @param {string} t the transition type.
|
||||||
|
* @param {boolean} plural whether there were multiple users undergoing the same
|
||||||
|
* transition.
|
||||||
|
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||||
|
* @returns {string} the written English equivalent of the transition.
|
||||||
|
*/
|
||||||
|
_getDescriptionForTransition(t, plural, repeats) {
|
||||||
|
const beConjugated = plural ? "were" : "was";
|
||||||
|
const invitation = "their invitation" + (plural || (repeats > 1) ? "s" : "");
|
||||||
|
|
||||||
|
let res = null;
|
||||||
|
const map = {
|
||||||
|
"joined": "joined",
|
||||||
|
"left": "left",
|
||||||
|
"joined_and_left": "joined and left",
|
||||||
|
"left_and_joined": "left and rejoined",
|
||||||
|
"invite_reject": "rejected " + invitation,
|
||||||
|
"invite_withdrawal": "had " + invitation + " withdrawn",
|
||||||
|
"invited": beConjugated + " invited",
|
||||||
|
"banned": beConjugated + " banned",
|
||||||
|
"unbanned": beConjugated + " unbanned",
|
||||||
|
"kicked": beConjugated + " kicked",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(map).includes(t)) {
|
||||||
|
res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a written English string representing `items`, with an optional limit on
|
||||||
|
* the number of items included in the result. If specified and if the length of
|
||||||
|
*`items` is greater than the limit, the string "and n others" will be appended onto
|
||||||
|
* the result.
|
||||||
|
* If `items` is empty, returns the empty string. If there is only one item, return
|
||||||
|
* it.
|
||||||
|
* @param {string[]} items the items to construct a string from.
|
||||||
|
* @param {number?} itemLimit the number by which to limit the list.
|
||||||
|
* @returns {string} a string constructed by joining `items` with a comma between each
|
||||||
|
* item, but with the last item appended as " and [lastItem]".
|
||||||
|
*/
|
||||||
|
_renderCommaSeparatedList(items, itemLimit) {
|
||||||
|
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||||
|
items.length - itemLimit, 0
|
||||||
|
);
|
||||||
|
if (items.length === 0) {
|
||||||
|
return "";
|
||||||
|
} else if (items.length === 1) {
|
||||||
|
return items[0];
|
||||||
|
} else if (remaining) {
|
||||||
|
items = items.slice(0, itemLimit);
|
||||||
|
const other = " other" + (remaining > 1 ? "s" : "");
|
||||||
|
return items.join(', ') + ' and ' + remaining + other;
|
||||||
|
} else {
|
||||||
|
const lastItem = items.pop();
|
||||||
|
return items.join(', ') + ' and ' + lastItem;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderAvatars: function(roomMembers) {
|
||||||
|
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
|
||||||
return (
|
return (
|
||||||
<MemberAvatar
|
<MemberAvatar key={m.userId} member={m} width={14} height={14} />
|
||||||
key={e.getId()}
|
|
||||||
member={e.sender}
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{avatars}
|
{avatars}
|
||||||
|
@ -134,74 +270,155 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
_getTransitionSequence: function(events) {
|
||||||
let summary = null;
|
return events.map(this._getTransition);
|
||||||
|
},
|
||||||
|
|
||||||
// Reorder events so that joins come before leaves
|
/**
|
||||||
let eventsToRender = this.props.events;
|
* Label a given membership event, `e`, where `getContent().membership` has
|
||||||
|
* changed for each transition allowed by the Matrix protocol. This attempts to
|
||||||
// Filter out those who joined, then left
|
* label the membership changes that occur in `../../../TextForEvent.js`.
|
||||||
let filteredEvents = eventsToRender.filter(
|
* @param {MatrixEvent} e the membership change event to label.
|
||||||
(e) => {
|
* @returns {string?} the transition type given to this event. This defaults to `null`
|
||||||
return eventsToRender.filter(
|
* if a transition is not recognised.
|
||||||
(e2) => {
|
*/
|
||||||
return e.getSender() === e2.getSender()
|
_getTransition: function(e) {
|
||||||
&& e.event.content.membership !== e2.event.content.membership;
|
switch (e.mxEvent.getContent().membership) {
|
||||||
|
case 'invite': return 'invited';
|
||||||
|
case 'ban': return 'banned';
|
||||||
|
case 'join': return 'joined';
|
||||||
|
case 'leave':
|
||||||
|
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
|
||||||
|
switch (e.mxEvent.getPrevContent().membership) {
|
||||||
|
case 'invite': return 'invite_reject';
|
||||||
|
default: return 'left';
|
||||||
}
|
}
|
||||||
).length === 0;
|
}
|
||||||
|
switch (e.mxEvent.getPrevContent().membership) {
|
||||||
|
case 'invite': return 'invite_withdrawal';
|
||||||
|
case 'ban': return 'unbanned';
|
||||||
|
case 'join': return 'kicked';
|
||||||
|
default: return 'left';
|
||||||
|
}
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getAggregate: function(userEvents) {
|
||||||
|
// A map of aggregate type to arrays of display names. Each aggregate type
|
||||||
|
// is a comma-delimited string of transitions, e.g. "joined,left,kicked".
|
||||||
|
// The array of display names is the array of users who went through that
|
||||||
|
// sequence during eventsToRender.
|
||||||
|
const aggregate = {
|
||||||
|
// $aggregateType : []:string
|
||||||
|
};
|
||||||
|
// A map of aggregate types to the indices that order them (the index of
|
||||||
|
// the first event for a given transition sequence)
|
||||||
|
const aggregateIndices = {
|
||||||
|
// $aggregateType : int
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = Object.keys(userEvents);
|
||||||
|
users.forEach(
|
||||||
|
(userId) => {
|
||||||
|
const firstEvent = userEvents[userId][0];
|
||||||
|
const displayName = firstEvent.displayName;
|
||||||
|
|
||||||
|
const seq = this._getTransitionSequence(userEvents[userId]);
|
||||||
|
if (!aggregate[seq]) {
|
||||||
|
aggregate[seq] = [];
|
||||||
|
aggregateIndices[seq] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregate[seq].push(displayName);
|
||||||
|
|
||||||
|
if (aggregateIndices[seq] === -1 ||
|
||||||
|
firstEvent.index < aggregateIndices[seq]) {
|
||||||
|
aggregateIndices[seq] = firstEvent.index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let joinAndLeft = (eventsToRender.length - filteredEvents.length) / 2;
|
return {
|
||||||
if (joinAndLeft <= 0 || joinAndLeft % 1 !== 0) {
|
names: aggregate,
|
||||||
joinAndLeft = null;
|
indices: aggregateIndices,
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
|
||||||
let joinEvents = filteredEvents.filter((ev) => {
|
render: function() {
|
||||||
return ev.event.content.membership === 'join';
|
const eventsToRender = this.props.events;
|
||||||
});
|
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||||
|
const expanded = this.state.expanded || fewEvents;
|
||||||
|
|
||||||
let leaveEvents = filteredEvents.filter((ev) => {
|
|
||||||
return ev.event.content.membership === 'leave';
|
|
||||||
});
|
|
||||||
|
|
||||||
let fewEvents = eventsToRender.length < this.props.threshold;
|
|
||||||
let expanded = this.state.expanded || fewEvents;
|
|
||||||
let expandedEvents = null;
|
let expandedEvents = null;
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
expandedEvents = this.props.children;
|
expandedEvents = this.props.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatars = this._renderAvatars(joinEvents.concat(leaveEvents));
|
if (fewEvents) {
|
||||||
|
return (
|
||||||
let toggleButton = null;
|
<div className="mx_MemberEventListSummary">
|
||||||
let summaryContainer = null;
|
{expandedEvents}
|
||||||
if (!fewEvents) {
|
|
||||||
summary = this._renderSummary(joinEvents, leaveEvents);
|
|
||||||
toggleButton = (
|
|
||||||
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
|
||||||
{expanded ? 'collapse' : 'expand'}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
let plural = (joinEvents.length + leaveEvents.length > 0) ? 'others' : 'users';
|
|
||||||
let noun = (joinAndLeft === 1 ? 'user' : plural);
|
|
||||||
|
|
||||||
summaryContainer = (
|
|
||||||
<div className="mx_EventTile_line">
|
|
||||||
<div className="mx_EventTile_info">
|
|
||||||
<span className="mx_MemberEventListSummary_avatars">
|
|
||||||
{avatars}
|
|
||||||
</span>
|
|
||||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
|
||||||
{summary}{joinAndLeft? '. ' + joinAndLeft + ' ' + noun + ' joined and left' : ''}
|
|
||||||
</span>
|
|
||||||
{toggleButton}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map user IDs to an array of objects:
|
||||||
|
const userEvents = {
|
||||||
|
// $userId : [{
|
||||||
|
// // The original event
|
||||||
|
// mxEvent: e,
|
||||||
|
// // The display name of the user (if not, then user ID)
|
||||||
|
// displayName: e.target.name || userId,
|
||||||
|
// // The original index of the event in this.props.events
|
||||||
|
// index: index,
|
||||||
|
// }]
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarMembers = [];
|
||||||
|
eventsToRender.forEach((e, index) => {
|
||||||
|
const userId = e.getStateKey();
|
||||||
|
// Initialise a user's events
|
||||||
|
if (!userEvents[userId]) {
|
||||||
|
userEvents[userId] = [];
|
||||||
|
avatarMembers.push(e.target);
|
||||||
|
}
|
||||||
|
userEvents[userId].push({
|
||||||
|
mxEvent: e,
|
||||||
|
displayName: e.target.name || userId,
|
||||||
|
index: index,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const aggregate = this._getAggregate(userEvents);
|
||||||
|
|
||||||
|
// Sort types by order of lowest event index within sequence
|
||||||
|
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
||||||
|
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
||||||
|
);
|
||||||
|
|
||||||
|
const avatars = this._renderAvatars(avatarMembers);
|
||||||
|
const summary = this._renderSummary(aggregate.names, orderedTransitionSequences);
|
||||||
|
const toggleButton = (
|
||||||
|
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
||||||
|
{expanded ? 'collapse' : 'expand'}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
const summaryContainer = (
|
||||||
|
<div className="mx_EventTile_line">
|
||||||
|
<div className="mx_EventTile_info">
|
||||||
|
<span className="mx_MemberEventListSummary_avatars">
|
||||||
|
{avatars}
|
||||||
|
</span>
|
||||||
|
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||||
|
{summary}
|
||||||
|
</span>
|
||||||
|
{toggleButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberEventListSummary">
|
<div className="mx_MemberEventListSummary">
|
||||||
{summaryContainer}
|
{summaryContainer}
|
||||||
|
|
|
@ -73,7 +73,7 @@ module.exports = React.createClass({
|
||||||
getValue: function() {
|
getValue: function() {
|
||||||
var value;
|
var value;
|
||||||
if (this.refs.select) {
|
if (this.refs.select) {
|
||||||
value = reverseRoles[ this.refs.select.value ];
|
value = reverseRoles[this.refs.select.value];
|
||||||
if (this.refs.custom) {
|
if (this.refs.custom) {
|
||||||
if (value === undefined) value = parseInt( this.refs.custom.value );
|
if (value === undefined) value = parseInt( this.refs.custom.value );
|
||||||
}
|
}
|
||||||
|
@ -86,10 +86,10 @@ module.exports = React.createClass({
|
||||||
if (this.state.custom) {
|
if (this.state.custom) {
|
||||||
var input;
|
var input;
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
input = <span>{ this.props.value }</span>
|
input = <span>{ this.props.value }</span>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>
|
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>;
|
||||||
}
|
}
|
||||||
customPicker = <span> of { input }</span>;
|
customPicker = <span> of { input }</span>;
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ module.exports = React.createClass({
|
||||||
<option value="Moderator">Moderator (50)</option>
|
<option value="Moderator">Moderator (50)</option>
|
||||||
<option value="Admin">Admin (100)</option>
|
<option value="Admin">Admin (100)</option>
|
||||||
<option value="Custom">Custom level</option>
|
<option value="Custom">Custom level</option>
|
||||||
</select>
|
</select>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -69,9 +69,19 @@ var TintableSvg = React.createClass({
|
||||||
width={ this.props.width }
|
width={ this.props.width }
|
||||||
height={ this.props.height }
|
height={ this.props.height }
|
||||||
onLoad={ this.onLoad }
|
onLoad={ this.onLoad }
|
||||||
|
tabIndex="-1"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register with the Tinter so that we will be told if the tint changes
|
||||||
|
Tinter.registerTintable(function() {
|
||||||
|
if (TintableSvg.mounts) {
|
||||||
|
Object.keys(TintableSvg.mounts).forEach((id) => {
|
||||||
|
TintableSvg.mounts[id].tint();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = TintableSvg;
|
module.exports = TintableSvg;
|
||||||
|
|
|
@ -56,7 +56,7 @@ module.exports = React.createClass({
|
||||||
<div>
|
<div>
|
||||||
<ul className="mx_UserSelector_UserIdList" ref="list">
|
<ul className="mx_UserSelector_UserIdList" ref="list">
|
||||||
{this.props.selected_users.map(function(user_id, i) {
|
{this.props.selected_users.map(function(user_id, i) {
|
||||||
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>
|
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/>
|
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/>
|
||||||
|
|
|
@ -52,12 +52,24 @@ module.exports = React.createClass({
|
||||||
this._onCaptchaLoaded();
|
this._onCaptchaLoaded();
|
||||||
} else {
|
} else {
|
||||||
console.log("Loading recaptcha script...");
|
console.log("Loading recaptcha script...");
|
||||||
var scriptTag = document.createElement('script');
|
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded()};
|
var protocol = global.location.protocol;
|
||||||
scriptTag.setAttribute(
|
if (protocol === "file:") {
|
||||||
'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"
|
var warning = document.createElement('div');
|
||||||
);
|
// XXX: fix hardcoded app URL. Better solutions include:
|
||||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
// * jumping straight to a hosted captcha page (but we don't support that yet)
|
||||||
|
// * embedding the captcha in an iframe (if that works)
|
||||||
|
// * using a better captcha lib
|
||||||
|
warning.innerHTML = "Robot check is currently unavailable on desktop - please use a <a href='https://riot.im/app'>web browser</a>.";
|
||||||
|
this.refs.recaptchaContainer.appendChild(warning);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var scriptTag = document.createElement('script');
|
||||||
|
scriptTag.setAttribute(
|
||||||
|
'src', protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"
|
||||||
|
);
|
||||||
|
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -89,7 +101,7 @@ module.exports = React.createClass({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: e.toString(),
|
errorText: e.toString(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -106,6 +118,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div ref="recaptchaContainer">
|
<div ref="recaptchaContainer">
|
||||||
This Home Server would like to make sure you are not a robot
|
This Home Server would like to make sure you are not a robot
|
||||||
|
<br/>
|
||||||
<div id={DIV_ID}></div>
|
<div id={DIV_ID}></div>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,7 +70,7 @@ export const PasswordAuthEntry = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onPasswordFieldChange: function (ev) {
|
_onPasswordFieldChange: function(ev) {
|
||||||
// enable the submit button iff the password is non-empty
|
// enable the submit button iff the password is non-empty
|
||||||
this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
|
this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
|
||||||
},
|
},
|
||||||
|
@ -209,4 +209,4 @@ export function getEntryComponentForLoginType(loginType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return FallbackAuthEntry;
|
return FallbackAuthEntry;
|
||||||
};
|
}
|
||||||
|
|
|
@ -38,6 +38,16 @@ module.exports = React.createClass({
|
||||||
defaultEmail: React.PropTypes.string,
|
defaultEmail: React.PropTypes.string,
|
||||||
defaultUsername: React.PropTypes.string,
|
defaultUsername: React.PropTypes.string,
|
||||||
defaultPassword: React.PropTypes.string,
|
defaultPassword: React.PropTypes.string,
|
||||||
|
teamsConfig: React.PropTypes.shape({
|
||||||
|
// Email address to request new teams
|
||||||
|
supportEmail: React.PropTypes.string,
|
||||||
|
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||||
|
// The displayed name of the team
|
||||||
|
"name": React.PropTypes.string,
|
||||||
|
// The domain of team email addresses
|
||||||
|
"domain": React.PropTypes.string,
|
||||||
|
})).required,
|
||||||
|
}),
|
||||||
|
|
||||||
// A username that will be used if no username is entered.
|
// A username that will be used if no username is entered.
|
||||||
// Specifying this param will also warn the user that entering
|
// Specifying this param will also warn the user that entering
|
||||||
|
@ -62,7 +72,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
fieldValid: {}
|
fieldValid: {},
|
||||||
|
selectedTeam: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -105,10 +116,11 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_doSubmit: function() {
|
_doSubmit: function() {
|
||||||
|
let email = this.refs.email.value.trim();
|
||||||
var promise = this.props.onRegisterClick({
|
var promise = this.props.onRegisterClick({
|
||||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||||
password: this.refs.password.value.trim(),
|
password: this.refs.password.value.trim(),
|
||||||
email: this.refs.email.value.trim()
|
email: email,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
|
@ -133,17 +145,37 @@ module.exports = React.createClass({
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_isUniEmail: function(email) {
|
||||||
|
return email.endsWith('.ac.uk') || email.endsWith('.edu');
|
||||||
|
},
|
||||||
|
|
||||||
validateField: function(field_id) {
|
validateField: function(field_id) {
|
||||||
var pwd1 = this.refs.password.value.trim();
|
var pwd1 = this.refs.password.value.trim();
|
||||||
var pwd2 = this.refs.passwordConfirm.value.trim()
|
var pwd2 = this.refs.passwordConfirm.value.trim();
|
||||||
|
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case FIELD_EMAIL:
|
case FIELD_EMAIL:
|
||||||
this.markFieldValid(
|
const email = this.refs.email.value;
|
||||||
field_id,
|
if (this.props.teamsConfig && this._isUniEmail(email)) {
|
||||||
this.refs.email.value == '' || Email.looksValid(this.refs.email.value),
|
const matchingTeam = this.props.teamsConfig.teams.find(
|
||||||
"RegistrationForm.ERR_EMAIL_INVALID"
|
(team) => {
|
||||||
);
|
return email.split('@').pop() === team.domain;
|
||||||
|
}
|
||||||
|
) || null;
|
||||||
|
this.setState({
|
||||||
|
selectedTeam: matchingTeam,
|
||||||
|
showSupportEmail: !matchingTeam,
|
||||||
|
});
|
||||||
|
this.props.onTeamSelected(matchingTeam);
|
||||||
|
} else {
|
||||||
|
this.props.onTeamSelected(null);
|
||||||
|
this.setState({
|
||||||
|
selectedTeam: null,
|
||||||
|
showSupportEmail: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const valid = email === '' || Email.looksValid(email);
|
||||||
|
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||||
break;
|
break;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
// XXX: SPEC-1
|
// XXX: SPEC-1
|
||||||
|
@ -224,15 +256,36 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var emailSection, registerButton;
|
var emailSection, belowEmailSection, registerButton;
|
||||||
if (this.props.showEmail) {
|
if (this.props.showEmail) {
|
||||||
emailSection = (
|
emailSection = (
|
||||||
<input type="text" ref="email"
|
<input type="text" ref="email"
|
||||||
autoFocus={true} placeholder="Email address (optional)"
|
autoFocus={true} placeholder="Email address (optional)"
|
||||||
defaultValue={this.props.defaultEmail}
|
defaultValue={this.props.defaultEmail}
|
||||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||||
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
|
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||||
|
value={self.state.email}/>
|
||||||
);
|
);
|
||||||
|
if (this.props.teamsConfig) {
|
||||||
|
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||||
|
belowEmailSection = (
|
||||||
|
<p className="mx_Login_support">
|
||||||
|
Sorry, but your university is not registered with us just yet.
|
||||||
|
Email us on
|
||||||
|
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||||
|
{this.props.teamsConfig.supportEmail}
|
||||||
|
</a>
|
||||||
|
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
} else if (this.state.selectedTeam) {
|
||||||
|
belowEmailSection = (
|
||||||
|
<p className="mx_Login_support">
|
||||||
|
You are registering with {this.state.selectedTeam.name}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.props.onRegisterClick) {
|
if (this.props.onRegisterClick) {
|
||||||
registerButton = (
|
registerButton = (
|
||||||
|
@ -242,31 +295,31 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var placeholderUserName = "User name";
|
var placeholderUserName = "User name";
|
||||||
if (this.props.guestUsername) {
|
if (this.props.guestUsername) {
|
||||||
placeholderUserName += " (default: " + this.props.guestUsername + ")"
|
placeholderUserName += " (default: " + this.props.guestUsername + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
{emailSection}
|
{emailSection}
|
||||||
<br />
|
{belowEmailSection}
|
||||||
<input type="text" ref="username"
|
<input type="text" ref="username"
|
||||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||||
onBlur={function() {self.validateField(FIELD_USERNAME)}} />
|
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
|
||||||
<br />
|
<br />
|
||||||
{ this.props.guestUsername ?
|
{ this.props.guestUsername ?
|
||||||
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
|
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
|
||||||
}
|
}
|
||||||
<input type="password" ref="password"
|
<input type="password" ref="password"
|
||||||
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
|
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
|
||||||
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
|
onBlur={function() {self.validateField(FIELD_PASSWORD);}}
|
||||||
placeholder="Password" defaultValue={this.props.defaultPassword} />
|
placeholder="Password" defaultValue={this.props.defaultPassword} />
|
||||||
<br />
|
<br />
|
||||||
<input type="password" ref="passwordConfirm"
|
<input type="password" ref="passwordConfirm"
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
|
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
|
||||||
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
|
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
|
||||||
defaultValue={this.props.defaultPassword} />
|
defaultValue={this.props.defaultPassword} />
|
||||||
<br />
|
<br />
|
||||||
{registerButton}
|
{registerButton}
|
||||||
|
|
|
@ -67,7 +67,7 @@ module.exports = React.createClass({
|
||||||
configVisible: !this.props.withToggleButton ||
|
configVisible: !this.props.withToggleButton ||
|
||||||
(this.props.customHsUrl !== this.props.defaultHsUrl) ||
|
(this.props.customHsUrl !== this.props.defaultHsUrl) ||
|
||||||
(this.props.customIsUrl !== this.props.defaultIsUrl)
|
(this.props.customIsUrl !== this.props.defaultIsUrl)
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onHomeserverChanged: function(ev) {
|
onHomeserverChanged: function(ev) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import MFileBody from './MFileBody';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { decryptFile } from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
|
|
||||||
export default class MAudioBody extends React.Component {
|
export default class MAudioBody extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -29,7 +29,9 @@ export default class MAudioBody extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
playing: false,
|
playing: false,
|
||||||
decryptedUrl: null,
|
decryptedUrl: null,
|
||||||
}
|
decryptedBlob: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
onPlayToggle() {
|
onPlayToggle() {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -49,29 +51,45 @@ export default class MAudioBody extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
var content = this.props.mxEvent.getContent();
|
var content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
decryptFile(content.file).done((url) => {
|
var decryptedBlob;
|
||||||
|
decryptFile(content.file).then(function(blob) {
|
||||||
|
decryptedBlob = blob;
|
||||||
|
return readBlobAsDataUri(decryptedBlob);
|
||||||
|
}).done((url) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
decryptedUrl: url
|
decryptedUrl: url,
|
||||||
|
decryptedBlob: decryptedBlob,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.warn("Unable to decrypt attachment: ", err)
|
console.warn("Unable to decrypt attachment: ", err);
|
||||||
// Set a placeholder image when we can't decrypt the image.
|
this.setState({
|
||||||
this.refs.image.src = "img/warning.svg";
|
error: err,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
|
||||||
|
if (this.state.error !== null) {
|
||||||
|
return (
|
||||||
|
<span className="mx_MAudioBody" ref="body">
|
||||||
|
<img src="img/warning.svg" width="16" height="16"/>
|
||||||
|
Error decrypting audio
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
// Need to decrypt the attachment
|
// Need to decrypt the attachment
|
||||||
// The attachment is decrypted in componentDidMount.
|
// The attachment is decrypted in componentDidMount.
|
||||||
// For now add an img tag with a spinner.
|
// For now add an img tag with a 16x16 spinner.
|
||||||
|
// Not sure how tall the audio player is so not sure how tall it should actually be.
|
||||||
return (
|
return (
|
||||||
<span className="mx_MAudioBody">
|
<span className="mx_MAudioBody">
|
||||||
<img src="img/spinner.gif" ref="image"
|
<img src="img/spinner.gif" alt={content.body} width="16" height="16"/>
|
||||||
alt={content.body} />
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +99,7 @@ export default class MAudioBody extends React.Component {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MAudioBody">
|
<span className="mx_MAudioBody">
|
||||||
<audio src={contentUrl} controls />
|
<audio src={contentUrl} controls />
|
||||||
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
|
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,120 +21,332 @@ import filesize from 'filesize';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import {decryptFile} from '../../../utils/DecryptFile';
|
import {decryptFile} from '../../../utils/DecryptFile';
|
||||||
|
import Tinter from '../../../Tinter';
|
||||||
|
import request from 'browser-request';
|
||||||
|
import q from 'q';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
|
|
||||||
|
// A cached tinted copy of "img/download.svg"
|
||||||
|
var tintedDownloadImageURL;
|
||||||
|
// Track a list of mounted MFileBody instances so that we can update
|
||||||
|
// the "img/download.svg" when the tint changes.
|
||||||
|
var nextMountId = 0;
|
||||||
|
const mounts = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the tinted copy of "img/download.svg" when the tint changes.
|
||||||
|
*/
|
||||||
|
function updateTintedDownloadImage() {
|
||||||
|
// Download the svg as an XML document.
|
||||||
|
// We could cache the XML response here, but since the tint rarely changes
|
||||||
|
// it's probably not worth it.
|
||||||
|
// Also note that we can't use fetch here because fetch doesn't support
|
||||||
|
// file URLs, which the download image will be if we're running from
|
||||||
|
// the filesystem (like in an Electron wrapper).
|
||||||
|
request({uri: "img/download.svg"}, (err, response, body) => {
|
||||||
|
if (err) return;
|
||||||
|
|
||||||
|
const svg = new DOMParser().parseFromString(body, "image/svg+xml");
|
||||||
|
// Apply the fixups to the XML.
|
||||||
|
const fixups = Tinter.calcSvgFixups([{contentDocument: svg}]);
|
||||||
|
Tinter.applySvgFixups(fixups);
|
||||||
|
// Encoded the fixed up SVG as a data URL.
|
||||||
|
const svgString = new XMLSerializer().serializeToString(svg);
|
||||||
|
tintedDownloadImageURL = "data:image/svg+xml;base64," + window.btoa(svgString);
|
||||||
|
// Notify each mounted MFileBody that the URL has changed.
|
||||||
|
Object.keys(mounts).forEach(function(id) {
|
||||||
|
mounts[id].tint();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Tinter.registerTintable(updateTintedDownloadImage);
|
||||||
|
|
||||||
|
// User supplied content can contain scripts, we have to be careful that
|
||||||
|
// we don't accidentally run those script within the same origin as the
|
||||||
|
// client. Otherwise those scripts written by remote users can read
|
||||||
|
// the access token and end-to-end keys that are in local storage.
|
||||||
|
//
|
||||||
|
// For attachments downloaded directly from the homeserver we can use
|
||||||
|
// Content-Security-Policy headers to disable script execution.
|
||||||
|
//
|
||||||
|
// But attachments with end-to-end encryption are more difficult to handle.
|
||||||
|
// We need to decrypt the attachment on the client and then display it.
|
||||||
|
// To display the attachment we need to turn the decrypted bytes into a URL.
|
||||||
|
//
|
||||||
|
// There are two ways to turn bytes into URLs, data URL and blob URLs.
|
||||||
|
// Data URLs aren't suitable for downloading a file because Chrome has a
|
||||||
|
// 2MB limit on the size of URLs that can be viewed in the browser or
|
||||||
|
// downloaded. This limit does not seem to apply when the url is used as
|
||||||
|
// the source attribute of an image tag.
|
||||||
|
//
|
||||||
|
// Blob URLs are generated using window.URL.createObjectURL and unforuntately
|
||||||
|
// for our purposes they inherit the origin of the page that created them.
|
||||||
|
// This means that any scripts that run when the URL is viewed will be able
|
||||||
|
// to access local storage.
|
||||||
|
//
|
||||||
|
// The easiest solution is to host the code that generates the blob URL on
|
||||||
|
// a different domain to the client.
|
||||||
|
// Another possibility is to generate the blob URL within a sandboxed iframe.
|
||||||
|
// The downside of using a second domain is that it complicates hosting,
|
||||||
|
// the downside of using a sandboxed iframe is that the browers are overly
|
||||||
|
// restrictive in what you are allowed to do with the generated URL.
|
||||||
|
//
|
||||||
|
// For now given how unusable the blobs generated in sandboxed iframes are we
|
||||||
|
// default to using a renderer hosted on "usercontent.riot.im". This is
|
||||||
|
// overridable so that people running their own version of the client can
|
||||||
|
// choose a different renderer.
|
||||||
|
//
|
||||||
|
// To that end the first version of the blob generation will be the following
|
||||||
|
// html:
|
||||||
|
//
|
||||||
|
// <html><head><script>
|
||||||
|
// window.onmessage=function(e){eval("("+e.data.code+")")(e)}
|
||||||
|
// </script></head><body></body></html>
|
||||||
|
//
|
||||||
|
// This waits to receive a message event sent using the window.postMessage API.
|
||||||
|
// When it receives the event it evals a javascript function in data.code and
|
||||||
|
// runs the function passing the event as an argument.
|
||||||
|
//
|
||||||
|
// In particular it means that the rendering function can be written as a
|
||||||
|
// ordinary javascript function which then is turned into a string using
|
||||||
|
// toString().
|
||||||
|
//
|
||||||
|
const DEFAULT_CROSS_ORIGIN_RENDERER = "https://usercontent.riot.im/v1.html";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the attachment inside the iframe.
|
||||||
|
* We can't use imported libraries here so this has to be vanilla JS.
|
||||||
|
*/
|
||||||
|
function remoteRender(event) {
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.id = "img";
|
||||||
|
img.src = data.imgSrc;
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.id = "a";
|
||||||
|
a.rel = data.rel;
|
||||||
|
a.target = data.target;
|
||||||
|
a.download = data.download;
|
||||||
|
a.style = data.style;
|
||||||
|
a.href = window.URL.createObjectURL(data.blob);
|
||||||
|
a.appendChild(img);
|
||||||
|
a.appendChild(document.createTextNode(data.textContent));
|
||||||
|
|
||||||
|
const body = document.body;
|
||||||
|
// Don't display scrollbars if the link takes more than one line
|
||||||
|
// to display.
|
||||||
|
body.style = "margin: 0px; overflow: hidden";
|
||||||
|
body.appendChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the tint inside the iframe.
|
||||||
|
* We can't use imported libraries here so this has to be vanilla JS.
|
||||||
|
*/
|
||||||
|
function remoteSetTint(event) {
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
const img = document.getElementById("img");
|
||||||
|
img.src = data.imgSrc;
|
||||||
|
img.style = data.imgStyle;
|
||||||
|
|
||||||
|
const a = document.getElementById("a");
|
||||||
|
a.style = data.style;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current CSS style for a DOMElement.
|
||||||
|
* @param {HTMLElement} element The element to get the current style of.
|
||||||
|
* @return {string} The CSS style encoded as a string.
|
||||||
|
*/
|
||||||
|
function computedStyle(element) {
|
||||||
|
if (!element) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const style = window.getComputedStyle(element, null);
|
||||||
|
var cssText = style.cssText;
|
||||||
|
if (cssText == "") {
|
||||||
|
// Firefox doesn't implement ".cssText" for computed styles.
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
||||||
|
for (var i = 0; i < style.length; i++) {
|
||||||
|
cssText += style[i] + ":";
|
||||||
|
cssText += style.getPropertyValue(style[i]) + ";";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cssText;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MFileBody',
|
displayName: 'MFileBody',
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
decryptedUrl: (this.props.decryptedUrl ? this.props.decryptedUrl : null),
|
decryptedBlob: (this.props.decryptedBlob ? this.props.decryptedBlob : null),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
appConfig: React.PropTypes.object,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a human readable label for the file attachment to use as
|
||||||
|
* link text.
|
||||||
|
*
|
||||||
|
* @params {Object} content The "content" key of the matrix event.
|
||||||
|
* @return {string} the human readable link text for the attachment.
|
||||||
|
*/
|
||||||
presentableTextForFile: function(content) {
|
presentableTextForFile: function(content) {
|
||||||
var linkText = 'Attachment';
|
var linkText = 'Attachment';
|
||||||
if (content.body && content.body.length > 0) {
|
if (content.body && content.body.length > 0) {
|
||||||
|
// The content body should be the name of the file including a
|
||||||
|
// file extension.
|
||||||
linkText = content.body;
|
linkText = content.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
var additionals = [];
|
if (content.info && content.info.size) {
|
||||||
if (content.info) {
|
// If we know the size of the file then add it as human readable
|
||||||
// if (content.info.mimetype && content.info.mimetype.length > 0) {
|
// string to the end of the link text so that the user knows how
|
||||||
// additionals.push(content.info.mimetype);
|
// big a file they are downloading.
|
||||||
// }
|
// The content.info also contains a MIME-type but we don't display
|
||||||
if (content.info.size) {
|
// it since it is "ugly", users generally aren't aware what it
|
||||||
additionals.push(filesize(content.info.size));
|
// means and the type of the attachment can usually be inferrered
|
||||||
}
|
// from the file extension.
|
||||||
}
|
linkText += ' (' + filesize(content.info.size) + ')';
|
||||||
|
|
||||||
if (additionals.length > 0) {
|
|
||||||
linkText += ' (' + additionals.join(', ') + ')';
|
|
||||||
}
|
}
|
||||||
return linkText;
|
return linkText;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getContentUrl: function() {
|
_getContentUrl: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||||
return this.state.decryptedUrl;
|
|
||||||
} else {
|
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
// Add this to the list of mounted components to receive notifications
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
// when the tint changes.
|
||||||
decryptFile(content.file).done((url) => {
|
this.id = nextMountId++;
|
||||||
this.setState({
|
mounts[this.id] = this;
|
||||||
decryptedUrl: url,
|
this.tint();
|
||||||
});
|
},
|
||||||
}, (err) => {
|
|
||||||
console.warn("Unable to decrypt attachment: ", err)
|
componentWillUnmount: function() {
|
||||||
// Set a placeholder image when we can't decrypt the image.
|
// Remove this from the list of mounted components
|
||||||
this.refs.image.src = "img/warning.svg";
|
delete mounts[this.id];
|
||||||
});
|
},
|
||||||
|
|
||||||
|
tint: function() {
|
||||||
|
// Update our tinted copy of "img/download.svg"
|
||||||
|
if (this.refs.downloadImage) {
|
||||||
|
this.refs.downloadImage.src = tintedDownloadImageURL;
|
||||||
|
}
|
||||||
|
if (this.refs.iframe) {
|
||||||
|
// If the attachment is encrypted then the download image
|
||||||
|
// will be inside the iframe so we wont be able to update
|
||||||
|
// it directly.
|
||||||
|
this.refs.iframe.contentWindow.postMessage({
|
||||||
|
code: remoteSetTint.toString(),
|
||||||
|
imgSrc: tintedDownloadImageURL,
|
||||||
|
style: computedStyle(this.refs.dummyLink),
|
||||||
|
}, "*");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
|
||||||
const text = this.presentableTextForFile(content);
|
const text = this.presentableTextForFile(content);
|
||||||
|
const isEncrypted = content.file !== undefined;
|
||||||
|
const fileName = content.body && content.body.length > 0 ? content.body : "Attachment";
|
||||||
|
const contentUrl = this._getContentUrl();
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
if (isEncrypted) {
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (this.state.decryptedBlob === null) {
|
||||||
|
// Need to decrypt the attachment
|
||||||
|
// Wait for the user to click on the link before downloading
|
||||||
|
// and decrypting the attachment.
|
||||||
|
var decrypting = false;
|
||||||
|
const decrypt = () => {
|
||||||
|
if (decrypting) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
decrypting = true;
|
||||||
|
decryptFile(content.file).then((blob) => {
|
||||||
|
this.setState({
|
||||||
|
decryptedBlob: blob,
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn("Unable to decrypt attachment: ", err);
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
description: "Error decrypting attachment"
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
decrypting = false;
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Need to decrypt the attachment
|
return (
|
||||||
// The attachment is decrypted in componentDidMount.
|
<span className="mx_MFileBody" ref="body">
|
||||||
// For now add an img tag with a spinner.
|
<div className="mx_MImageBody_download">
|
||||||
|
<a href="javascript:void(0)" onClick={decrypt}>
|
||||||
|
Decrypt {text}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the iframe loads we tell it to render a download link
|
||||||
|
const onIframeLoad = (ev) => {
|
||||||
|
ev.target.contentWindow.postMessage({
|
||||||
|
code: remoteRender.toString(),
|
||||||
|
imgSrc: tintedDownloadImageURL,
|
||||||
|
style: computedStyle(this.refs.dummyLink),
|
||||||
|
blob: this.state.decryptedBlob,
|
||||||
|
// Set a download attribute for encrypted files so that the file
|
||||||
|
// will have the correct name when the user tries to download it.
|
||||||
|
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||||
|
download: fileName,
|
||||||
|
target: "_blank",
|
||||||
|
textContent: "Download " + text,
|
||||||
|
}, "*");
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the attachment is encryped then put the link inside an iframe.
|
||||||
|
let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
|
||||||
|
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) {
|
||||||
|
renderer_url = this.context.appConfig.cross_origin_renderer_url;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody" ref="body">
|
<span className="mx_MFileBody">
|
||||||
<img src="img/spinner.gif" ref="image"
|
<div className="mx_MImageBody_download">
|
||||||
alt={content.body} />
|
<div style={{display: "none"}}>
|
||||||
|
{/*
|
||||||
|
* Add dummy copy of the "a" tag
|
||||||
|
* We'll use it to learn how the download link
|
||||||
|
* would have been styled if it was rendered inline.
|
||||||
|
*/}
|
||||||
|
<a ref="dummyLink"/>
|
||||||
|
</div>
|
||||||
|
<iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe"/>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
} else if (contentUrl) {
|
||||||
|
// If the attachment is not encrypted then we check whether we
|
||||||
const contentUrl = this._getContentUrl();
|
// are being displayed in the room timeline or in a list of
|
||||||
|
// files in the right hand side of the screen.
|
||||||
const fileName = content.body && content.body.length > 0 ? content.body : "Attachment";
|
|
||||||
|
|
||||||
var downloadAttr = undefined;
|
|
||||||
if (this.state.decryptedUrl) {
|
|
||||||
// If the file is encrypted then we MUST download it rather than displaying it
|
|
||||||
// because Firefox is vunerable to XSS attacks in data:// URLs
|
|
||||||
// and all browsers are vunerable to XSS attacks in blob: URLs
|
|
||||||
// created with window.URL.createObjectURL
|
|
||||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=255107
|
|
||||||
// See https://w3c.github.io/FileAPI/#originOfBlobURL
|
|
||||||
//
|
|
||||||
// This is not a problem for unencrypted links because they are
|
|
||||||
// either fetched from a different domain so are safe because of
|
|
||||||
// the same-origin policy or they are fetch from the same domain,
|
|
||||||
// in which case we trust that the homeserver will set a
|
|
||||||
// Content-Security-Policy that disables script execution.
|
|
||||||
// It is reasonable to trust the homeserver in that case since
|
|
||||||
// it is the same domain that controls this javascript.
|
|
||||||
//
|
|
||||||
// We can't apply the same workaround for encrypted files because
|
|
||||||
// we can't supply HTTP headers when the user clicks on a blob:
|
|
||||||
// or data:// uri.
|
|
||||||
//
|
|
||||||
// We should probably provide a download attribute anyway so that
|
|
||||||
// the file will have the correct name when the user tries to
|
|
||||||
// download it. We can't provide a Content-Disposition header
|
|
||||||
// like we would for HTTP.
|
|
||||||
downloadAttr = fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentUrl) {
|
|
||||||
if (this.props.tileShape === "file_grid") {
|
if (this.props.tileShape === "file_grid") {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MImageBody_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
|
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank">
|
||||||
{ fileName }
|
{ fileName }
|
||||||
</a>
|
</a>
|
||||||
<div className="mx_MImageBody_size">
|
<div className="mx_MImageBody_size">
|
||||||
|
@ -148,8 +360,8 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MImageBody_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
|
<a href={contentUrl} target="_blank" rel="noopener">
|
||||||
<TintableSvg src="img/download.svg" width="12" height="14"/>
|
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
|
||||||
Download {text}
|
Download {text}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -160,7 +372,7 @@ module.exports = React.createClass({
|
||||||
var extra = text ? (': ' + text) : '';
|
var extra = text ? (': ' + text) : '';
|
||||||
return <span className="mx_MFileBody">
|
return <span className="mx_MFileBody">
|
||||||
Invalid file{extra}
|
Invalid file{extra}
|
||||||
</span>
|
</span>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,7 +23,8 @@ import ImageUtils from '../../../ImageUtils';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import {decryptFile} from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
|
import q from 'q';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MImageBody',
|
displayName: 'MImageBody',
|
||||||
|
@ -31,11 +32,17 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
/* the MatrixEvent to show */
|
/* the MatrixEvent to show */
|
||||||
mxEvent: React.PropTypes.object.isRequired,
|
mxEvent: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/* called when the image has loaded */
|
||||||
|
onWidgetLoad: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
decryptedUrl: null,
|
decryptedUrl: null,
|
||||||
|
decryptedThumbnailUrl: null,
|
||||||
|
decryptedBlob: null,
|
||||||
|
error: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -94,7 +101,9 @@ module.exports = React.createClass({
|
||||||
_getThumbUrl: function() {
|
_getThumbUrl: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
// TODO: Decrypt and use the thumbnail file if one is present.
|
if (this.state.decryptedThumbnailUrl) {
|
||||||
|
return this.state.decryptedThumbnailUrl;
|
||||||
|
}
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
|
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
|
||||||
|
@ -106,15 +115,34 @@ module.exports = React.createClass({
|
||||||
this.fixupHeight();
|
this.fixupHeight();
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
decryptFile(content.file).done((url) => {
|
var thumbnailPromise = q(null);
|
||||||
this.setState({
|
if (content.info.thumbnail_file) {
|
||||||
decryptedUrl: url,
|
thumbnailPromise = decryptFile(
|
||||||
|
content.info.thumbnail_file
|
||||||
|
).then(function(blob) {
|
||||||
|
return readBlobAsDataUri(blob);
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}
|
||||||
console.warn("Unable to decrypt attachment: ", err)
|
var decryptedBlob;
|
||||||
|
thumbnailPromise.then((thumbnailUrl) => {
|
||||||
|
return decryptFile(content.file).then(function(blob) {
|
||||||
|
decryptedBlob = blob;
|
||||||
|
return readBlobAsDataUri(blob);
|
||||||
|
}).then((contentUrl) => {
|
||||||
|
this.setState({
|
||||||
|
decryptedUrl: contentUrl,
|
||||||
|
decryptedThumbnailUrl: thumbnailUrl,
|
||||||
|
decryptedBlob: decryptedBlob,
|
||||||
|
});
|
||||||
|
this.props.onWidgetLoad();
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn("Unable to decrypt attachment: ", err);
|
||||||
// Set a placeholder image when we can't decrypt the image.
|
// Set a placeholder image when we can't decrypt the image.
|
||||||
this.refs.image.src = "img/warning.svg";
|
this.setState({
|
||||||
});
|
error: err,
|
||||||
|
});
|
||||||
|
}).done();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -152,6 +180,15 @@ module.exports = React.createClass({
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
|
||||||
|
if (this.state.error !== null) {
|
||||||
|
return (
|
||||||
|
<span className="mx_MImageBody" ref="body">
|
||||||
|
<img src="img/warning.svg" width="16" height="16"/>
|
||||||
|
Error decrypting image
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
|
|
||||||
// Need to decrypt the attachment
|
// Need to decrypt the attachment
|
||||||
|
@ -159,8 +196,15 @@ module.exports = React.createClass({
|
||||||
// For now add an img tag with a spinner.
|
// For now add an img tag with a spinner.
|
||||||
return (
|
return (
|
||||||
<span className="mx_MImageBody" ref="body">
|
<span className="mx_MImageBody" ref="body">
|
||||||
<img className="mx_MImageBody_thumbnail" src="img/spinner.gif" ref="image"
|
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||||
alt={content.body} />
|
"display": "flex",
|
||||||
|
"alignItems": "center",
|
||||||
|
"width": "100%",
|
||||||
|
}}>
|
||||||
|
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
|
||||||
|
"margin": "auto",
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -177,7 +221,7 @@ module.exports = React.createClass({
|
||||||
onMouseEnter={this.onImageEnter}
|
onMouseEnter={this.onImageEnter}
|
||||||
onMouseLeave={this.onImageLeave} />
|
onMouseLeave={this.onImageLeave} />
|
||||||
</a>
|
</a>
|
||||||
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
|
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (content.body) {
|
} else if (content.body) {
|
||||||
|
|
|
@ -21,16 +21,26 @@ import MFileBody from './MFileBody';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Model from '../../../Modal';
|
import Model from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { decryptFile } from '../../../utils/DecryptFile';
|
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||||
import q from 'q';
|
import q from 'q';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MVideoBody',
|
displayName: 'MVideoBody',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
/* the MatrixEvent to show */
|
||||||
|
mxEvent: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/* called when the video has loaded */
|
||||||
|
onWidgetLoad: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
decryptedUrl: null,
|
decryptedUrl: null,
|
||||||
decryptedThumbnailUrl: null,
|
decryptedThumbnailUrl: null,
|
||||||
|
decryptedBlob: null,
|
||||||
|
error: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -83,19 +93,29 @@ module.exports = React.createClass({
|
||||||
if (content.info.thumbnail_file) {
|
if (content.info.thumbnail_file) {
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(
|
||||||
content.info.thumbnail_file
|
content.info.thumbnail_file
|
||||||
);
|
).then(function(blob) {
|
||||||
|
return readBlobAsDataUri(blob);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
var decryptedBlob;
|
||||||
thumbnailPromise.then((thumbnailUrl) => {
|
thumbnailPromise.then((thumbnailUrl) => {
|
||||||
decryptFile(content.file).then((contentUrl) => {
|
return decryptFile(content.file).then(function(blob) {
|
||||||
|
decryptedBlob = blob;
|
||||||
|
return readBlobAsDataUri(blob);
|
||||||
|
}).then((contentUrl) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
decryptedUrl: contentUrl,
|
decryptedUrl: contentUrl,
|
||||||
decryptedThumbnailUrl: thumbnailUrl,
|
decryptedThumbnailUrl: thumbnailUrl,
|
||||||
|
decryptedBlob: decryptedBlob,
|
||||||
});
|
});
|
||||||
|
this.props.onWidgetLoad();
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn("Unable to decrypt attachment: ", err)
|
console.warn("Unable to decrypt attachment: ", err);
|
||||||
// Set a placeholder image when we can't decrypt the image.
|
// Set a placeholder image when we can't decrypt the image.
|
||||||
this.refs.image.src = "img/warning.svg";
|
this.setState({
|
||||||
|
error: err,
|
||||||
|
});
|
||||||
}).done();
|
}).done();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -103,14 +123,29 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
|
||||||
|
if (this.state.error !== null) {
|
||||||
|
return (
|
||||||
|
<span className="mx_MVideoBody" ref="body">
|
||||||
|
<img src="img/warning.svg" width="16" height="16"/>
|
||||||
|
Error decrypting video
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
// Need to decrypt the attachment
|
// Need to decrypt the attachment
|
||||||
// The attachment is decrypted in componentDidMount.
|
// The attachment is decrypted in componentDidMount.
|
||||||
// For now add an img tag with a spinner.
|
// For now add an img tag with a spinner.
|
||||||
return (
|
return (
|
||||||
<span className="mx_MImageBody" ref="body">
|
<span className="mx_MVideoBody" ref="body">
|
||||||
<img className="mx_MImageBody_thumbnail" src="img/spinner.gif" ref="image"
|
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||||
alt={content.body} />
|
"display": "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-items": "center",
|
||||||
|
"width": "100%",
|
||||||
|
}}>
|
||||||
|
<img src="img/spinner.gif" alt={content.body} width="16" height="16"/>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -141,7 +176,7 @@ module.exports = React.createClass({
|
||||||
controls preload={preload} autoPlay={false}
|
controls preload={preload} autoPlay={false}
|
||||||
height={height} width={width} poster={poster}>
|
height={height} width={width} poster={poster}>
|
||||||
</video>
|
</video>
|
||||||
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
|
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,7 +27,6 @@ var sdk = require('../../../index');
|
||||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||||
var Modal = require("../../../Modal");
|
var Modal = require("../../../Modal");
|
||||||
var SdkConfig = require('../../../SdkConfig');
|
var SdkConfig = require('../../../SdkConfig');
|
||||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -201,7 +200,7 @@ module.exports = React.createClass({
|
||||||
global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId());
|
global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onStarterLinkClick: function(starterLink, ev) {
|
onStarterLinkClick: function(starterLink, ev) {
|
||||||
|
@ -213,16 +212,6 @@ module.exports = React.createClass({
|
||||||
// which requires the user to click through and THEN we can open the link in a new tab because
|
// which requires the user to click through and THEN we can open the link in a new tab because
|
||||||
// the window.open command occurs in the same stack frame as the onClick callback.
|
// the window.open command occurs in the same stack frame as the onClick callback.
|
||||||
|
|
||||||
let integrationsEnabled = UserSettingsStore.isFeatureEnabled("integration_management");
|
|
||||||
if (!integrationsEnabled) {
|
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: "Integrations disabled",
|
|
||||||
description: "You need to enable the Labs option 'Integrations Management' in your Riot user settings first.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go fetch a scalar token
|
// Go fetch a scalar token
|
||||||
let scalarClient = new ScalarAuthClient();
|
let scalarClient = new ScalarAuthClient();
|
||||||
scalarClient.connect().then(() => {
|
scalarClient.connect().then(() => {
|
||||||
|
@ -241,8 +230,8 @@ module.exports = React.createClass({
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let width = window.screen.width > 1024 ? 1024 : window.screen.width;
|
let width = window.screen.width > 1024 ? 1024 : window.screen.width;
|
||||||
let height = window.screen.height > 800 ? 800 : window.screen.height;
|
let height = window.screen.height > 800 ? 800 : window.screen.height;
|
||||||
let left = (window.screen.width - width) / 2;
|
let left = (window.screen.width - width) / 2;
|
||||||
let top = (window.screen.height - height) / 2;
|
let top = (window.screen.height - height) / 2;
|
||||||
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);
|
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);
|
||||||
|
@ -304,4 +293,3 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -103,13 +103,13 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
if (oldCanonicalAlias !== this.state.canonicalAlias) {
|
if (oldCanonicalAlias !== this.state.canonicalAlias) {
|
||||||
console.log("AliasSettings: Updating canonical alias");
|
console.log("AliasSettings: Updating canonical alias");
|
||||||
promises = [ q.all(promises).then(
|
promises = [q.all(promises).then(
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
this.props.roomId, "m.room.canonical_alias", {
|
this.props.roomId, "m.room.canonical_alias", {
|
||||||
alias: this.state.canonicalAlias
|
alias: this.state.canonicalAlias
|
||||||
}, ""
|
}, ""
|
||||||
)
|
)
|
||||||
) ];
|
)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return promises;
|
return promises;
|
||||||
|
@ -281,7 +281,7 @@ module.exports = React.createClass({
|
||||||
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
|
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
|
||||||
editable={ self.props.canSetAliases }
|
editable={ self.props.canSetAliases }
|
||||||
initialValue={ alias } />
|
initialValue={ alias } />
|
||||||
<div className="mx_RoomSettings_deleteAlias">
|
<div className="mx_RoomSettings_deleteAlias mx_filterFlipColor">
|
||||||
{ deleteButton }
|
{ deleteButton }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -297,7 +297,7 @@ module.exports = React.createClass({
|
||||||
placeholder={ "New address (e.g. #foo:" + localDomain + ")" }
|
placeholder={ "New address (e.g. #foo:" + localDomain + ")" }
|
||||||
blurToCancel={ false }
|
blurToCancel={ false }
|
||||||
onValueChanged={ self.onAliasAdded } />
|
onValueChanged={ self.onAliasAdded } />
|
||||||
<div className="mx_RoomSettings_addAlias">
|
<div className="mx_RoomSettings_addAlias mx_filterFlipColor">
|
||||||
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
||||||
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -135,7 +135,7 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
var boundClick = this._onColorSchemeChanged.bind(this, i)
|
var boundClick = this._onColorSchemeChanged.bind(this, i);
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSettings_roomColor"
|
<div className="mx_RoomSettings_roomColor"
|
||||||
key={ "room_color_" + i }
|
key={ "room_color_" + i }
|
||||||
|
|
|
@ -121,13 +121,13 @@ module.exports = React.createClass({
|
||||||
onChange={ this.onGlobalDisableUrlPreviewChange }
|
onChange={ this.onGlobalDisableUrlPreviewChange }
|
||||||
checked={ this.state.globalDisableUrlPreview } />
|
checked={ this.state.globalDisableUrlPreview } />
|
||||||
Disable URL previews by default for participants in this room
|
Disable URL previews by default for participants in this room
|
||||||
</label>
|
</label>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
disableRoomPreviewUrls =
|
disableRoomPreviewUrls =
|
||||||
<label>
|
<label>
|
||||||
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
|
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
|
||||||
</label>
|
</label>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -93,8 +93,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
joinText = (<span>
|
joinText = (<span>
|
||||||
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice')}}
|
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}}
|
||||||
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video') }}
|
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video'); }}
|
||||||
href="#">video</a>.
|
href="#">video</a>.
|
||||||
</span>);
|
</span>);
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue