Merge remote-tracking branch 'origin/develop' into rav/hotkey-ux
This commit is contained in:
commit
6dd46d532a
124 changed files with 1837 additions and 654 deletions
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
71
.eslintrc.js
Normal file
71
.eslintrc.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
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"],
|
||||||
|
"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
|
|
@ -4,3 +4,6 @@ node_js:
|
||||||
install:
|
install:
|
||||||
- npm install
|
- npm install
|
||||||
- (cd node_modules/matrix-js-sdk && npm install)
|
- (cd node_modules/matrix-js-sdk && npm install)
|
||||||
|
script:
|
||||||
|
- npm run test
|
||||||
|
- ./.travis-test-riot.sh
|
||||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,3 +1,30 @@
|
||||||
|
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)
|
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)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.3...v0.8.4)
|
||||||
|
|
|
@ -19,7 +19,7 @@ npm install
|
||||||
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
|
||||||
|
|
15
package.json
15
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.8.4",
|
"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",
|
||||||
|
@ -58,7 +59,7 @@
|
||||||
"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",
|
"commonmark": "^0.27.0",
|
||||||
"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",
|
||||||
|
@ -67,13 +68,14 @@
|
||||||
"react-dom": "^15.4.0",
|
"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",
|
||||||
|
@ -85,9 +87,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",
|
||||||
|
|
|
@ -56,5 +56,5 @@ module.exports = {
|
||||||
}
|
}
|
||||||
return 'img/' + images[total % images.length] + '.png';
|
return 'img/' + images[total % images.length] + '.png';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -159,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) {
|
||||||
|
|
|
@ -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);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -53,5 +53,5 @@ module.exports = {
|
||||||
return Math.floor(heightMulti * fullHeight);
|
return Math.floor(heightMulti * fullHeight);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -55,29 +55,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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
134
src/Markdown.js
134
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var parsed = this.parser.parse(this.input);
|
||||||
|
var rendered = this.renderer.render(parsed);
|
||||||
|
|
||||||
|
this.renderer.paragraph = real_paragraph;
|
||||||
|
|
||||||
|
return rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
const real_options = Object.assign({}, marked_options, {
|
toPlaintext() {
|
||||||
renderer: real_renderer,
|
const real_paragraph = this.renderer.paragraph;
|
||||||
});
|
|
||||||
const real_parser = new marked.Parser(real_options);
|
// The default `out` function only sends the input through an XML
|
||||||
return real_parser.parse(this._copyTokens());
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
79
src/Modal.js
79
src/Modal.js
|
@ -19,6 +19,55 @@ 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let _counter = 0;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DialogContainerId: "mx_Dialog_Container",
|
DialogContainerId: "mx_Dialog_Container",
|
||||||
|
@ -36,20 +85,46 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
createDialog: function(Element, props, className) {
|
createDialog: function(Element, props, className) {
|
||||||
var self = this;
|
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: function(loader, props, className) {
|
||||||
|
var self = this;
|
||||||
// never call this via modal.close() from onFinished() otherwise it will loop
|
// never call this via modal.close() from onFinished() otherwise it will loop
|
||||||
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());
|
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// don't attempt to reuse the same AsyncWrapper for different dialogs,
|
||||||
|
// otherwise we'll get confused.
|
||||||
|
const modalCount = _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!
|
||||||
var dialog = (
|
var dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + className}>
|
<div className={"mx_Dialog_wrapper " + className}>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
<Element {...props} onFinished={closeDialog}/>
|
<AsyncWrapper key={modalCount} loader={loader} {...props} onFinished={closeDialog}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
|
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,7 +88,7 @@ var Notifier = {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.load();
|
e.load();
|
||||||
e.play();
|
e.play();
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -371,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 = {
|
||||||
|
|
|
@ -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})`
|
||||||
|
|
|
@ -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,7 +331,7 @@ 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() {}));
|
||||||
|
|
||||||
|
|
|
@ -254,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();
|
||||||
|
@ -386,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,7 +149,7 @@ 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.
|
// List of functions to call when the tint changes.
|
||||||
|
@ -185,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;
|
||||||
|
@ -194,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];
|
||||||
|
@ -210,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");
|
||||||
|
|
||||||
|
@ -224,6 +225,19 @@ module.exports = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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
|
||||||
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
|
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
|
||||||
// keeping it here for now.
|
// keeping it here for now.
|
||||||
|
|
|
@ -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);
|
||||||
}
|
};
|
||||||
|
|
|
@ -46,4 +46,4 @@ module.exports = {
|
||||||
return names.join(', ') + ' and ' + lastPerson + ' are typing';
|
return names.join(', ') + ' and ' + lastPerson + ' are typing';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
84
src/async-components/views/dialogs/ExportE2eKeysDialog.js
Normal file
84
src/async-components/views/dialogs/ExportE2eKeysDialog.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
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 sdk from '../../../index';
|
||||||
|
|
||||||
|
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'ExportE2eKeysDialog',
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
collectedPassword: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_onPassphraseFormSubmit: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
console.log(this.refs.passphrase1.value);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
let content;
|
||||||
|
if (!this.state.collectedPassword) {
|
||||||
|
content = (
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<p>
|
||||||
|
This process will allow 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>
|
||||||
|
<form onSubmit={this._onPassphraseFormSubmit}>
|
||||||
|
<div className="mx_TextInputDialog_label">
|
||||||
|
<label htmlFor="passphrase1">Enter passphrase</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input ref="passphrase1" id="passphrase1"
|
||||||
|
className="mx_TextInputDialog_input"
|
||||||
|
autoFocus={true} size="64" type="password"/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<input className="mx_Dialog_primary" type="submit" value="Export" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_exportE2eKeysDialog">
|
||||||
|
<div className="mx_Dialog_title">
|
||||||
|
Export room keys
|
||||||
|
</div>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,8 +75,6 @@ import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInvit
|
||||||
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';
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -160,8 +160,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:
|
||||||
|
@ -170,28 +170,28 @@ 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}
|
||||||
/>
|
/>;
|
||||||
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.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ module.exports = React.createClass({
|
||||||
getChildContext: function() {
|
getChildContext: function() {
|
||||||
return {
|
return {
|
||||||
appConfig: this.props.config,
|
appConfig: this.props.config,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -456,6 +456,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;
|
||||||
|
@ -586,6 +589,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
|
||||||
*/
|
*/
|
||||||
|
@ -687,6 +734,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) {
|
||||||
|
@ -979,7 +1036,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');
|
||||||
|
@ -1003,6 +1060,7 @@ module.exports = React.createClass({
|
||||||
defaultHsUrl={this.getDefaultHsUrl()}
|
defaultHsUrl={this.getDefaultHsUrl()}
|
||||||
defaultIsUrl={this.getDefaultIsUrl()}
|
defaultIsUrl={this.getDefaultIsUrl()}
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
|
teamsConfig={this.props.config.teamsConfig}
|
||||||
customHsUrl={this.getCurrentHsUrl()}
|
customHsUrl={this.getCurrentHsUrl()}
|
||||||
customIsUrl={this.getCurrentIsUrl()}
|
customIsUrl={this.getCurrentIsUrl()}
|
||||||
registrationUrl={this.props.registrationUrl}
|
registrationUrl={this.props.registrationUrl}
|
||||||
|
|
|
@ -19,7 +19,7 @@ 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;
|
const MILLIS_IN_DAY = 86400000;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,14 @@ 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 TYPING_AVATARS_LIMIT = 2;
|
||||||
|
|
||||||
|
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',
|
||||||
|
@ -60,6 +68,13 @@ 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,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -78,6 +93,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() {
|
||||||
|
@ -104,35 +131,28 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// determine if we need to call onResize
|
// We don't need the actual height - just whether it is likely to have
|
||||||
_checkForResize: function(prevProps, prevState) {
|
|
||||||
// figure out the old height and the new height of the status bar. 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
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
// indicate other sizes.
|
// indicate other sizes.
|
||||||
var oldSize, newSize;
|
_getSize: function(state, props) {
|
||||||
|
if (state.syncState === "ERROR" ||
|
||||||
if (prevState.syncState === "ERROR") {
|
state.whoisTypingString ||
|
||||||
oldSize = 1;
|
props.numUnreadMessages ||
|
||||||
} else if (prevProps.tabCompleteEntries) {
|
!props.atEndOfLiveTimeline ||
|
||||||
oldSize = 0;
|
props.hasActiveCall) {
|
||||||
} else if (prevProps.hasUnsentMessages) {
|
return STATUS_BAR_EXPANDED;
|
||||||
oldSize = 2;
|
} else if (props.tabCompleteEntries) {
|
||||||
} else {
|
return STATUS_BAR_HIDDEN;
|
||||||
oldSize = 0;
|
} else if (props.hasUnsentMessages) {
|
||||||
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
}
|
}
|
||||||
|
return STATUS_BAR_HIDDEN;
|
||||||
|
},
|
||||||
|
|
||||||
if (this.state.syncState === "ERROR") {
|
// determine if we need to call onResize
|
||||||
newSize = 1;
|
_checkForResize: function(prevProps, prevState) {
|
||||||
} else if (this.props.tabCompleteEntries) {
|
// figure out the old height and the new height of the status bar.
|
||||||
newSize = 0;
|
return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state);
|
||||||
} 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 +193,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(TYPING_AVATARS_LIMIT)}
|
||||||
<span>.</span>
|
|
||||||
<span>.</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -184,6 +202,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() {
|
||||||
|
|
|
@ -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");
|
||||||
|
@ -758,7 +761,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;
|
||||||
|
|
||||||
|
@ -962,7 +965,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,
|
||||||
|
@ -1025,7 +1028,7 @@ module.exports = React.createClass({
|
||||||
if (scrollPanel) {
|
if (scrollPanel) {
|
||||||
scrollPanel.checkScroll();
|
scrollPanel.checkScroll();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
var lastRoomId;
|
var lastRoomId;
|
||||||
|
|
||||||
|
@ -1090,7 +1093,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) {
|
||||||
|
@ -1099,7 +1102,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
|
||||||
}
|
}
|
||||||
|
@ -1208,8 +1211,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();
|
||||||
|
|
||||||
|
@ -1331,6 +1335,18 @@ module.exports = React.createClass({
|
||||||
// no longer anything to do here
|
// no longer anything to do here
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onStatusBarVisible: function() {
|
||||||
|
this.setState({
|
||||||
|
statusBarVisible: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onStatusBarHidden: function() {
|
||||||
|
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) {
|
||||||
|
@ -1498,7 +1514,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
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');
|
||||||
|
|
||||||
|
@ -1513,7 +1529,9 @@ 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}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var aux = null;
|
var aux = null;
|
||||||
|
@ -1569,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
|
||||||
|
@ -1597,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 =
|
||||||
|
@ -1614,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
|
||||||
|
@ -1667,6 +1685,10 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||||
|
if (this.state.statusBarVisible) {
|
||||||
|
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">
|
||||||
|
@ -1689,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 }
|
||||||
|
|
|
@ -600,7 +600,7 @@ 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("Saved scroll state", this.scrollState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -564,9 +564,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -644,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();
|
||||||
}
|
}
|
||||||
|
@ -821,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);
|
||||||
|
|
||||||
|
@ -843,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();
|
||||||
|
@ -930,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);
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -33,6 +33,53 @@ var AccessibleButton = require('../views/elements/AccessibleButton');
|
||||||
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',
|
||||||
|
|
||||||
|
@ -94,6 +141,12 @@ module.exports = React.createClass({
|
||||||
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() {
|
||||||
|
@ -294,8 +347,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,
|
||||||
|
@ -343,34 +396,20 @@ module.exports = React.createClass({
|
||||||
_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() }
|
||||||
|
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
||||||
|
{ THEMES.map( this._renderThemeSelector ) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderUrlPreviewSelector: function() {
|
||||||
|
return <div className="mx_UserSettings_toggle">
|
||||||
<input id="urlPreviewsDisabled"
|
<input id="urlPreviewsDisabled"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
||||||
|
@ -379,22 +418,44 @@ module.exports = React.createClass({
|
||||||
<label htmlFor="urlPreviewsDisabled">
|
<label htmlFor="urlPreviewsDisabled">
|
||||||
Disable inline URL previews by default
|
Disable inline URL previews by default
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>;
|
||||||
</div>
|
},
|
||||||
{ settingsLabels.forEach( setting => {
|
|
||||||
<div className="mx_UserSettings_toggle">
|
_renderSyncedSetting: function(setting) {
|
||||||
|
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||||
<input id={ setting.id }
|
<input id={ setting.id }
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={ syncedSettings[setting.id] }
|
defaultChecked={ this._syncedSettings[setting.id] }
|
||||||
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
|
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
|
||||||
/>
|
/>
|
||||||
<label htmlFor={ setting.id }>
|
<label htmlFor={ setting.id }>
|
||||||
{ settings.label }
|
{ setting.label }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>;
|
||||||
})}
|
},
|
||||||
</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() {
|
||||||
|
@ -461,7 +522,7 @@ module.exports = React.createClass({
|
||||||
{features}
|
{features}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderDeactivateAccount: function() {
|
_renderDeactivateAccount: function() {
|
||||||
|
@ -545,10 +606,10 @@ module.exports = React.createClass({
|
||||||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</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>
|
||||||
<div className="mx_UserSettings_threepidButton">
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
<img src="img/icon_context_delete.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
|
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -570,7 +631,7 @@ module.exports = React.createClass({
|
||||||
blurToCancel={ false }
|
blurToCancel={ false }
|
||||||
onValueChanged={ this.onAddThreepidClicked } />
|
onValueChanged={ this.onAddThreepidClicked } />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_threepidButton">
|
<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>
|
||||||
|
@ -651,7 +712,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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -129,7 +129,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:' &&
|
||||||
|
@ -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 (
|
||||||
|
|
|
@ -49,6 +49,21 @@ module.exports = React.createClass({
|
||||||
email: React.PropTypes.string,
|
email: React.PropTypes.string,
|
||||||
username: React.PropTypes.string,
|
username: React.PropTypes.string,
|
||||||
guestAccessToken: React.PropTypes.string,
|
guestAccessToken: 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 suffix with which every team email address ends
|
||||||
|
"emailSuffix": React.PropTypes.string,
|
||||||
|
// The rooms to use during auto-join
|
||||||
|
"rooms": React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||||
|
"id": React.PropTypes.string,
|
||||||
|
"autoJoin": React.PropTypes.bool,
|
||||||
|
})),
|
||||||
|
})).required,
|
||||||
|
}),
|
||||||
|
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: React.PropTypes.string,
|
||||||
|
|
||||||
|
@ -169,6 +184,26 @@ module.exports = React.createClass({
|
||||||
accessToken: response.access_token
|
accessToken: response.access_token
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-join rooms
|
||||||
|
if (self.props.teamsConfig && self.props.teamsConfig.teams) {
|
||||||
|
for (let i = 0; i < self.props.teamsConfig.teams.length; i++) {
|
||||||
|
let team = self.props.teamsConfig.teams[i];
|
||||||
|
if (self.state.formVals.email.endsWith(team.emailSuffix)) {
|
||||||
|
console.log("User successfully registered with team " + team.name);
|
||||||
|
if (!team.rooms) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
team.rooms.forEach((room) => {
|
||||||
|
if (room.autoJoin) {
|
||||||
|
console.log("Auto-joining " + room.id);
|
||||||
|
MatrixClientPeg.get().joinRoom(room.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
@ -254,6 +289,7 @@ module.exports = React.createClass({
|
||||||
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.props.teamsConfig}
|
||||||
guestUsername={this.props.username}
|
guestUsername={this.props.username}
|
||||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
onError={this.onFormValidationFailed}
|
onError={this.onFormValidationFailed}
|
||||||
|
@ -297,7 +333,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 (
|
||||||
|
|
|
@ -42,7 +42,7 @@ module.exports = React.createClass({
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
defaultToInitialLetter: true
|
defaultToInitialLetter: true
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
|
|
@ -42,7 +42,7 @@ module.exports = React.createClass({
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
viewUserOnClick: false,
|
viewUserOnClick: false,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -64,7 +64,7 @@ module.exports = React.createClass({
|
||||||
props.width,
|
props.width,
|
||||||
props.height,
|
props.height,
|
||||||
props.resizeMethod)
|
props.resizeMethod)
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -78,7 +78,7 @@ module.exports = React.createClass({
|
||||||
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() {
|
||||||
|
|
|
@ -28,6 +28,15 @@ var AccessibleButton = require('../elements/AccessibleButton');
|
||||||
|
|
||||||
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: {
|
||||||
|
@ -72,15 +81,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) {
|
||||||
|
@ -120,15 +126,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();
|
||||||
|
@ -136,21 +142,16 @@ module.exports = React.createClass({
|
||||||
} else if (e.keyCode === 13) { // enter
|
} else if (e.keyCode === 13) { // enter
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (this.refs.textinput.value == '') {
|
||||||
|
// if there's nothing in the input box, submit the form
|
||||||
this.onButtonClick();
|
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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -180,7 +181,7 @@ module.exports = React.createClass({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(index) {
|
onClick: function(index) {
|
||||||
|
@ -316,13 +317,18 @@ 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:
|
||||||
|
// * 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 true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -362,6 +368,22 @@ module.exports = React.createClass({
|
||||||
return addrs;
|
return addrs;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_addInputToList: function() {
|
||||||
|
const addrType = Invite.getAddressType(this.refs.textinput.value);
|
||||||
|
if (addrType !== null) {
|
||||||
|
const inviteList = this.state.inviteList.slice();
|
||||||
|
inviteList.push(this.refs.textinput.value.trim());
|
||||||
|
this.setState({
|
||||||
|
inviteList: inviteList,
|
||||||
|
queryList: [],
|
||||||
|
});
|
||||||
|
return inviteList;
|
||||||
|
} else {
|
||||||
|
this.setState({ error: true });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
var AddressSelector = sdk.getComponent("elements.AddressSelector");
|
var AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||||
|
@ -395,13 +417,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 }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -28,6 +28,9 @@ module.exports = React.createClass({
|
||||||
addressList: React.PropTypes.array.isRequired,
|
addressList: React.PropTypes.array.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 +58,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 +67,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 +76,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() {
|
|
||||||
self.setState({
|
|
||||||
selected: index,
|
selected: index,
|
||||||
hover: true,
|
hover: true,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseLeave: function() {
|
onMouseLeave: function() {
|
||||||
|
@ -124,7 +121,7 @@ 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].userId} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -135,7 +132,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 +143,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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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,7 +52,7 @@ module.exports = React.createClass({
|
||||||
this._onCaptchaLoaded();
|
this._onCaptchaLoaded();
|
||||||
} else {
|
} else {
|
||||||
console.log("Loading recaptcha script...");
|
console.log("Loading recaptcha script...");
|
||||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded()};
|
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||||
var protocol = global.location.protocol;
|
var protocol = global.location.protocol;
|
||||||
if (protocol === "file:") {
|
if (protocol === "file:") {
|
||||||
var warning = document.createElement('div');
|
var warning = document.createElement('div');
|
||||||
|
@ -101,7 +101,7 @@ module.exports = React.createClass({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: e.toString(),
|
errorText: e.toString(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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 suffix with which every team email address ends
|
||||||
|
"emailSuffix": 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,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_doSubmit: function() {
|
_doSubmit: function() {
|
||||||
|
let email = this.refs.email.value.trim();
|
||||||
|
if (this.state.selectedTeam) {
|
||||||
|
email += "@" + this.state.selectedTeam.emailSuffix;
|
||||||
|
}
|
||||||
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) {
|
||||||
|
@ -119,6 +134,25 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSelectTeam: function(teamIndex) {
|
||||||
|
let team = this._getSelectedTeam(teamIndex);
|
||||||
|
if (team) {
|
||||||
|
this.refs.email.value = this.refs.email.value.split("@")[0];
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
selectedTeam: team,
|
||||||
|
showSupportEmail: teamIndex === "other",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_getSelectedTeam: function(teamIndex) {
|
||||||
|
if (this.props.teamsConfig &&
|
||||||
|
this.props.teamsConfig.teams[teamIndex]) {
|
||||||
|
return this.props.teamsConfig.teams[teamIndex];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if all fields were valid last time
|
* Returns true if all fields were valid last time
|
||||||
* they were validated.
|
* they were validated.
|
||||||
|
@ -135,15 +169,19 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
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(
|
let email = this.refs.email.value;
|
||||||
field_id,
|
if (this.props.teamsConfig) {
|
||||||
this.refs.email.value == '' || Email.looksValid(this.refs.email.value),
|
let team = this.state.selectedTeam;
|
||||||
"RegistrationForm.ERR_EMAIL_INVALID"
|
if (team) {
|
||||||
);
|
email = email + "@" + team.emailSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let 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
|
||||||
|
@ -222,17 +260,64 @@ module.exports = React.createClass({
|
||||||
return cls;
|
return cls;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderEmailInputSuffix: function() {
|
||||||
|
let suffix = null;
|
||||||
|
if (!this.state.selectedTeam) {
|
||||||
|
return suffix;
|
||||||
|
}
|
||||||
|
let team = this.state.selectedTeam;
|
||||||
|
if (team) {
|
||||||
|
suffix = "@" + team.emailSuffix;
|
||||||
|
}
|
||||||
|
return suffix;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var emailSection, registerButton;
|
var emailSection, teamSection, teamAdditionSupport, registerButton;
|
||||||
if (this.props.showEmail) {
|
if (this.props.showEmail) {
|
||||||
|
let emailSuffix = this._renderEmailInputSuffix();
|
||||||
emailSection = (
|
emailSection = (
|
||||||
|
<div>
|
||||||
<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}/>
|
||||||
|
{emailSuffix ? <input className="mx_Login_field" value={emailSuffix} disabled/> : null }
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
if (this.props.teamsConfig) {
|
||||||
|
teamSection = (
|
||||||
|
<select
|
||||||
|
defaultValue="-1"
|
||||||
|
className="mx_Login_field"
|
||||||
|
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||||
|
onChange={function(ev) {self.onSelectTeam(ev.target.value);}}
|
||||||
|
>
|
||||||
|
<option key="-1" value="-1">No team</option>
|
||||||
|
{this.props.teamsConfig.teams.map((t, index) => {
|
||||||
|
return (
|
||||||
|
<option key={index} value={index}>
|
||||||
|
{t.name}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<option key="-2" value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||||
|
teamAdditionSupport = (
|
||||||
|
<span>
|
||||||
|
If your team is not listed, email
|
||||||
|
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||||
|
{this.props.teamsConfig.supportEmail}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.props.onRegisterClick) {
|
if (this.props.onRegisterClick) {
|
||||||
registerButton = (
|
registerButton = (
|
||||||
|
@ -242,31 +327,34 @@ 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}>
|
||||||
|
{teamSection}
|
||||||
|
{teamAdditionSupport}
|
||||||
|
<br />
|
||||||
{emailSection}
|
{emailSection}
|
||||||
<br />
|
<br />
|
||||||
<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) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default class MAudioBody extends React.Component {
|
||||||
decryptedUrl: null,
|
decryptedUrl: null,
|
||||||
decryptedBlob: null,
|
decryptedBlob: null,
|
||||||
error: null,
|
error: null,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
onPlayToggle() {
|
onPlayToggle() {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -281,7 +281,7 @@ module.exports = React.createClass({
|
||||||
decryptedBlob: blob,
|
decryptedBlob: blob,
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn("Unable to decrypt attachment: ", err)
|
console.warn("Unable to decrypt attachment: ", err);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
description: "Error decrypting attachment"
|
description: "Error decrypting attachment"
|
||||||
});
|
});
|
||||||
|
@ -372,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>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -111,7 +111,7 @@ module.exports = React.createClass({
|
||||||
this.props.onWidgetLoad();
|
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.setState({
|
this.setState({
|
||||||
error: err,
|
error: err,
|
||||||
|
|
|
@ -200,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) {
|
||||||
|
|
|
@ -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>);
|
||||||
|
|
||||||
|
|
|
@ -259,11 +259,11 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
|
|
||||||
onEditClicked: function(e) {
|
onEditClicked: function(e) {
|
||||||
var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||||
var buttonRect = e.target.getBoundingClientRect()
|
var buttonRect = e.target.getBoundingClientRect();
|
||||||
|
|
||||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
var x = buttonRect.right + window.pageXOffset;
|
var x = buttonRect.right + window.pageXOffset;
|
||||||
var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19;
|
var y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||||
var self = this;
|
var self = this;
|
||||||
ContextualMenu.createMenu(MessageContextMenu, {
|
ContextualMenu.createMenu(MessageContextMenu, {
|
||||||
chevronOffset: 10,
|
chevronOffset: 10,
|
||||||
|
@ -293,7 +293,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
// If it is, we want to display the complete date along with the HH:MM:SS,
|
// If it is, we want to display the complete date along with the HH:MM:SS,
|
||||||
// rather than just HH:MM:SS.
|
// rather than just HH:MM:SS.
|
||||||
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
|
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
|
||||||
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1)
|
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1);
|
||||||
dayAfterEvent.setHours(0);
|
dayAfterEvent.setHours(0);
|
||||||
dayAfterEvent.setMinutes(0);
|
dayAfterEvent.setMinutes(0);
|
||||||
dayAfterEvent.setSeconds(0);
|
dayAfterEvent.setSeconds(0);
|
||||||
|
@ -366,10 +366,11 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onCryptoClicked: function(e) {
|
onCryptoClicked: function(e) {
|
||||||
var EncryptedEventDialog = sdk.getComponent("dialogs.EncryptedEventDialog");
|
|
||||||
var event = this.props.mxEvent;
|
var event = this.props.mxEvent;
|
||||||
|
|
||||||
Modal.createDialog(EncryptedEventDialog, {
|
Modal.createDialogAsync((cb) => {
|
||||||
|
require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb);
|
||||||
|
}, {
|
||||||
event: event,
|
event: event,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -465,7 +466,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
var editButton = (
|
var editButton = (
|
||||||
<img className="mx_EventTile_editButton" src="img/icon_context_message.svg" width="19" height="19" alt="Options" title="Options" onClick={this.onEditClicked} />
|
<span className="mx_EventTile_editButton" title="Options" onClick={this.onEditClicked} />
|
||||||
);
|
);
|
||||||
|
|
||||||
var e2e;
|
var e2e;
|
||||||
|
|
|
@ -60,13 +60,15 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
if (this.refs.description)
|
if (this.refs.description) {
|
||||||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
if (this.refs.description)
|
if (this.refs.description) {
|
||||||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -116,7 +118,7 @@ module.exports = React.createClass({
|
||||||
if (image) {
|
if (image) {
|
||||||
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
||||||
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={ image } onClick={ this.onImageClick }/>
|
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={ image } onClick={ this.onImageClick }/>
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default class MemberDeviceInfo extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
MemberDeviceInfo.displayName = 'MemberDeviceInfo';
|
MemberDeviceInfo.displayName = 'MemberDeviceInfo';
|
||||||
MemberDeviceInfo.propTypes = {
|
MemberDeviceInfo.propTypes = {
|
||||||
|
|
|
@ -65,7 +65,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
updating: 0,
|
updating: 0,
|
||||||
devicesLoading: true,
|
devicesLoading: true,
|
||||||
devices: null,
|
devices: null,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -203,7 +203,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
var cancelled = false;
|
var cancelled = false;
|
||||||
this._cancelDeviceList = function() { cancelled = true; }
|
this._cancelDeviceList = function() { cancelled = true; };
|
||||||
|
|
||||||
var client = this.props.matrixClient;
|
var client = this.props.matrixClient;
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -621,7 +621,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
<img src="img/create-big.svg" width="26" height="26" />
|
<img src="img/create-big.svg" width="26" height="26" />
|
||||||
</div>
|
</div>
|
||||||
<div className={labelClasses}><i>Start new chat</i></div>
|
<div className={labelClasses}><i>Start new chat</i></div>
|
||||||
</AccessibleButton>
|
</AccessibleButton>;
|
||||||
|
|
||||||
startChat = <div>
|
startChat = <div>
|
||||||
<h3>Direct chats</h3>
|
<h3>Direct chats</h3>
|
||||||
|
@ -655,7 +655,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
||||||
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||||
{giveOpLabel}
|
{giveOpLabel}
|
||||||
</AccessibleButton>
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
|
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
|
||||||
|
@ -673,7 +673,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
{banButton}
|
{banButton}
|
||||||
{giveModButton}
|
{giveModButton}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const memberName = this.props.member.name;
|
const memberName = this.props.member.name;
|
||||||
|
|
|
@ -32,7 +32,7 @@ var SHARE_HISTORY_WARNING =
|
||||||
Newly invited users will see the history of this room. <br/>
|
Newly invited users will see the history of this room. <br/>
|
||||||
If you'd prefer invited users not to see messages that were sent before they joined, <br/>
|
If you'd prefer invited users not to see messages that were sent before they joined, <br/>
|
||||||
turn off, 'Share message history with new users' in the settings for this room.
|
turn off, 'Share message history with new users' in the settings for this room.
|
||||||
</span>
|
</span>;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MemberList',
|
displayName: 'MemberList',
|
||||||
|
@ -338,8 +338,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
memberList.push(
|
memberList.push(
|
||||||
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} />
|
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} />
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
||||||
(this.user_last_modified_time === undefined ||
|
(this.user_last_modified_time === undefined ||
|
||||||
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime())
|
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime())
|
||||||
) {
|
) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
|
@ -222,20 +222,22 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
let e2eimg, e2etitle;
|
let e2eImg, e2eTitle, e2eClass;
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
|
if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
|
||||||
// FIXME: show a /!\ if there are untrusted devices in the room...
|
// FIXME: show a /!\ if there are untrusted devices in the room...
|
||||||
e2eimg = 'img/e2e-verified.svg';
|
e2eImg = 'img/e2e-verified.svg';
|
||||||
e2etitle = 'Encrypted room';
|
e2eTitle = 'Encrypted room';
|
||||||
|
e2eClass = 'mx_MessageComposer_e2eIcon';
|
||||||
} else {
|
} else {
|
||||||
e2eimg = 'img/e2e-unencrypted.svg';
|
e2eImg = 'img/e2e-unencrypted.svg';
|
||||||
e2etitle = 'Unencrypted room';
|
e2eTitle = 'Unencrypted room';
|
||||||
|
e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor';
|
||||||
}
|
}
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<img key="e2eIcon" className="mx_MessageComposer_e2eIcon" src={e2eimg} width="12" height="12"
|
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
||||||
alt={e2etitle} title={e2etitle}
|
alt={e2eTitle} title={e2eTitle}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
var callButton, videoCallButton, hangupButton;
|
var callButton, videoCallButton, hangupButton;
|
||||||
|
@ -331,6 +333,7 @@ export default class MessageComposer extends React.Component {
|
||||||
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
|
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
|
||||||
const className = classNames("mx_MessageComposer_format_button", {
|
const className = classNames("mx_MessageComposer_format_button", {
|
||||||
mx_MessageComposer_format_button_disabled: disabled,
|
mx_MessageComposer_format_button_disabled: disabled,
|
||||||
|
mx_filterFlipColor: true,
|
||||||
});
|
});
|
||||||
return <img className={className}
|
return <img className={className}
|
||||||
title={name}
|
title={name}
|
||||||
|
@ -355,11 +358,11 @@ export default class MessageComposer extends React.Component {
|
||||||
<div style={{flex: 1}}></div>
|
<div style={{flex: 1}}></div>
|
||||||
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
|
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
|
||||||
onMouseDown={this.onToggleMarkdownClicked}
|
onMouseDown={this.onToggleMarkdownClicked}
|
||||||
className="mx_MessageComposer_formatbar_markdown"
|
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||||
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
||||||
<img title="Hide Text Formatting Toolbar"
|
<img title="Hide Text Formatting Toolbar"
|
||||||
onClick={this.onToggleFormattingClicked}
|
onClick={this.onToggleFormattingClicked}
|
||||||
className="mx_MessageComposer_formatbar_cancel"
|
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||||
src="img/icon-text-cancel.svg" />
|
src="img/icon-text-cancel.svg" />
|
||||||
</div>
|
</div>
|
||||||
</div>: null
|
</div>: null
|
||||||
|
@ -367,7 +370,7 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
MessageComposer.propTypes = {
|
MessageComposer.propTypes = {
|
||||||
tabComplete: React.PropTypes.any,
|
tabComplete: React.PropTypes.any,
|
||||||
|
|
|
@ -443,12 +443,12 @@ export default class MessageComposerInput extends React.Component {
|
||||||
selection = this.state.editorState.getSelection();
|
selection = this.state.editorState.getSelection();
|
||||||
|
|
||||||
let modifyFn = {
|
let modifyFn = {
|
||||||
bold: text => `**${text}**`,
|
'bold': text => `**${text}**`,
|
||||||
italic: text => `*${text}*`,
|
'italic': text => `*${text}*`,
|
||||||
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
'underline': text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||||
strike: text => `~~${text}~~`,
|
'strike': text => `~~${text}~~`,
|
||||||
code: text => `\`${text}\``,
|
'code': text => `\`${text}\``,
|
||||||
blockquote: text => text.split('\n').map(line => `> ${line}\n`).join(''),
|
'blockquote': text => text.split('\n').map(line => `> ${line}\n`).join(''),
|
||||||
'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''),
|
'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''),
|
||||||
'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''),
|
'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''),
|
||||||
}[command];
|
}[command];
|
||||||
|
@ -462,8 +462,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState == null)
|
if (newState == null) {
|
||||||
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
|
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
|
||||||
|
}
|
||||||
|
|
||||||
if (newState != null) {
|
if (newState != null) {
|
||||||
this.setEditorState(newState);
|
this.setEditorState(newState);
|
||||||
|
@ -523,7 +524,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const md = new Markdown(contentText);
|
const md = new Markdown(contentText);
|
||||||
if (!md.isPlainText()) {
|
if (md.isPlainText()) {
|
||||||
|
contentText = md.toPlaintext();
|
||||||
|
} else {
|
||||||
contentHTML = md.toHTML();
|
contentHTML = md.toHTML();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -663,7 +666,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
const blockName = {
|
const blockName = {
|
||||||
'code-block': 'code',
|
'code-block': 'code',
|
||||||
blockquote: 'quote',
|
'blockquote': 'quote',
|
||||||
'unordered-list-item': 'bullet',
|
'unordered-list-item': 'bullet',
|
||||||
'ordered-list-item': 'numbullet',
|
'ordered-list-item': 'numbullet',
|
||||||
};
|
};
|
||||||
|
@ -716,7 +719,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
selection={selection} />
|
selection={selection} />
|
||||||
</div>
|
</div>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<img className="mx_MessageComposer_input_markdownIndicator"
|
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||||
onMouseDown={this.onMarkdownToggleClicked}
|
onMouseDown={this.onMarkdownToggleClicked}
|
||||||
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
||||||
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
||||||
|
@ -738,7 +741,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
MessageComposerInput.propTypes = {
|
MessageComposerInput.propTypes = {
|
||||||
tabComplete: React.PropTypes.any,
|
tabComplete: React.PropTypes.any,
|
||||||
|
|
|
@ -331,6 +331,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
const contentText = mdown.toPlaintext();
|
||||||
sendMessagePromise = isEmote ?
|
sendMessagePromise = isEmote ?
|
||||||
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
||||||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||||
|
|
|
@ -71,7 +71,7 @@ module.exports = React.createClass({
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
leftOffset: 0,
|
leftOffset: 0,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -81,7 +81,7 @@ module.exports = React.createClass({
|
||||||
// position.
|
// position.
|
||||||
return {
|
return {
|
||||||
suppressDisplay: !this.props.suppressAnimation,
|
suppressDisplay: !this.props.suppressAnimation,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
|
|
@ -183,8 +183,8 @@ module.exports = React.createClass({
|
||||||
'm.room.name', user_id
|
'm.room.name', user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>
|
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
|
||||||
cancel_button = <AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </AccessibleButton>
|
cancel_button = <AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.saving) {
|
if (this.props.saving) {
|
||||||
|
@ -194,7 +194,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (can_set_room_name) {
|
if (can_set_room_name) {
|
||||||
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
||||||
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />
|
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var searchStatus;
|
var searchStatus;
|
||||||
|
@ -233,7 +233,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (can_set_room_topic) {
|
if (can_set_room_topic) {
|
||||||
var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
||||||
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />
|
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
|
||||||
} else {
|
} else {
|
||||||
var topic;
|
var topic;
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
|
@ -302,7 +302,11 @@ module.exports = React.createClass({
|
||||||
rightPanel_buttons =
|
rightPanel_buttons =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
||||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
||||||
|
<<<<<<< HEAD
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
=======
|
||||||
|
</div>;
|
||||||
|
>>>>>>> origin/develop
|
||||||
}
|
}
|
||||||
|
|
||||||
var right_row;
|
var right_row;
|
||||||
|
|
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
||||||
isLoadingLeftRooms: false,
|
isLoadingLeftRooms: false,
|
||||||
lists: {},
|
lists: {},
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -338,7 +338,7 @@ module.exports = React.createClass({
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||||
|
|
||||||
var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset)
|
var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset);
|
||||||
// Make sure we don't go too far up, if the headers aren't sticky
|
// Make sure we don't go too far up, if the headers aren't sticky
|
||||||
top = (top < scrollAreaOffset) ? scrollAreaOffset : top;
|
top = (top < scrollAreaOffset) ? scrollAreaOffset : top;
|
||||||
// make sure we don't go too far down, if the headers aren't sticky
|
// make sure we don't go too far down, if the headers aren't sticky
|
||||||
|
@ -401,7 +401,7 @@ module.exports = React.createClass({
|
||||||
var stickyHeight = sticky.dataset.originalHeight;
|
var stickyHeight = sticky.dataset.originalHeight;
|
||||||
var stickyHeader = sticky.childNodes[0];
|
var stickyHeader = sticky.childNodes[0];
|
||||||
var topStuckHeight = stickyHeight * i;
|
var topStuckHeight = stickyHeight * i;
|
||||||
var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i)
|
var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i);
|
||||||
|
|
||||||
if (self.scrollAreaSufficient && stickyPosition < (scrollArea.scrollTop + topStuckHeight)) {
|
if (self.scrollAreaSufficient && stickyPosition < (scrollArea.scrollTop + topStuckHeight)) {
|
||||||
// Top stickies
|
// Top stickies
|
||||||
|
@ -520,7 +520,7 @@ module.exports = React.createClass({
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />;
|
||||||
|
|
||||||
}
|
}
|
||||||
}) }
|
}) }
|
||||||
|
|
|
@ -58,7 +58,7 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
busy: false
|
busy: false
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -96,7 +96,7 @@ module.exports = React.createClass({
|
||||||
emailMatchBlock = <div className="error">
|
emailMatchBlock = <div className="error">
|
||||||
Unable to ascertain that the address this invite was
|
Unable to ascertain that the address this invite was
|
||||||
sent to matches one associated with your account.
|
sent to matches one associated with your account.
|
||||||
</div>
|
</div>;
|
||||||
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) {
|
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) {
|
||||||
emailMatchBlock =
|
emailMatchBlock =
|
||||||
<div className="mx_RoomPreviewBar_warning">
|
<div className="mx_RoomPreviewBar_warning">
|
||||||
|
@ -107,7 +107,7 @@ module.exports = React.createClass({
|
||||||
This invitation was sent to <b><span className="email">{this.props.invitedEmail}</span></b>, which is not associated with this account.<br/>
|
This invitation was sent to <b><span className="email">{this.props.invitedEmail}</span></b>, which is not associated with this account.<br/>
|
||||||
You may wish to login with a different account, or add this email to this account.
|
You may wish to login with a different account, or add this email to this account.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
joinBlock = (
|
joinBlock = (
|
||||||
|
|
|
@ -404,7 +404,7 @@ module.exports = React.createClass({
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
var roomState = this.props.room.currentState;
|
var roomState = this.props.room.currentState;
|
||||||
return (roomState.mayClientSendStateEvent("m.room.join_rules", cli) &&
|
return (roomState.mayClientSendStateEvent("m.room.join_rules", cli) &&
|
||||||
roomState.mayClientSendStateEvent("m.room.guest_access", cli))
|
roomState.mayClientSendStateEvent("m.room.guest_access", cli));
|
||||||
},
|
},
|
||||||
|
|
||||||
onManageIntegrations(ev) {
|
onManageIntegrations(ev) {
|
||||||
|
@ -510,7 +510,7 @@ module.exports = React.createClass({
|
||||||
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||||
var EditableText = sdk.getComponent('elements.EditableText');
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
var Loader = sdk.getComponent("elements.Spinner")
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
var roomState = this.props.room.currentState;
|
var roomState = this.props.room.currentState;
|
||||||
|
@ -557,7 +557,7 @@ module.exports = React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
userLevelsSection = <div>No users have specific privileges in this room.</div>
|
userLevelsSection = <div>No users have specific privileges in this room.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var banned = this.props.room.getMembersWithMembership("ban");
|
var banned = this.props.room.getMembersWithMembership("ban");
|
||||||
|
@ -635,7 +635,7 @@ module.exports = React.createClass({
|
||||||
</label>);
|
</label>);
|
||||||
})) : (self.state.tags && self.state.tags.join) ? self.state.tags.join(", ") : ""
|
})) : (self.state.tags && self.state.tags.join) ? self.state.tags.join(", ") : ""
|
||||||
}
|
}
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no history_visibility, it is assumed to be 'shared'.
|
// If there is no history_visibility, it is assumed to be 'shared'.
|
||||||
|
@ -653,7 +653,7 @@ module.exports = React.createClass({
|
||||||
addressWarning =
|
addressWarning =
|
||||||
<div className="mx_RoomSettings_warning">
|
<div className="mx_RoomSettings_warning">
|
||||||
To link to a room it must have <a href="#addresses">an address</a>.
|
To link to a room it must have <a href="#addresses">an address</a>.
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var inviteGuestWarning;
|
var inviteGuestWarning;
|
||||||
|
@ -664,7 +664,7 @@ module.exports = React.createClass({
|
||||||
this.setState({ join_rule: "invite", guest_access: "can_join" });
|
this.setState({ join_rule: "invite", guest_access: "can_join" });
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}>Click here to fix</a>.
|
}}>Click here to fix</a>.
|
||||||
</div>
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var integrationsButton;
|
var integrationsButton;
|
||||||
|
|
|
@ -27,6 +27,7 @@ var ContextualMenu = require('../../structures/ContextualMenu');
|
||||||
var RoomNotifs = require('../../../RoomNotifs');
|
var RoomNotifs = require('../../../RoomNotifs');
|
||||||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||||
var AccessibleButton = require('../elements/AccessibleButton');
|
var AccessibleButton = require('../elements/AccessibleButton');
|
||||||
|
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomTile',
|
displayName: 'RoomTile',
|
||||||
|
@ -177,7 +178,8 @@ module.exports = React.createClass({
|
||||||
var self = this;
|
var self = this;
|
||||||
ContextualMenu.createMenu(RoomTagMenu, {
|
ContextualMenu.createMenu(RoomTagMenu, {
|
||||||
chevronOffset: 10,
|
chevronOffset: 10,
|
||||||
menuColour: "#FFFFFF",
|
// XXX: fix horrid hardcoding
|
||||||
|
menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF",
|
||||||
left: x,
|
left: x,
|
||||||
top: y,
|
top: y,
|
||||||
room: this.props.room,
|
room: this.props.room,
|
||||||
|
@ -220,7 +222,7 @@ module.exports = React.createClass({
|
||||||
var avatarContainerClasses = classNames({
|
var avatarContainerClasses = classNames({
|
||||||
'mx_RoomTile_avatar_container': true,
|
'mx_RoomTile_avatar_container': true,
|
||||||
'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu,
|
'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu,
|
||||||
})
|
});
|
||||||
|
|
||||||
var badgeClasses = classNames({
|
var badgeClasses = classNames({
|
||||||
'mx_RoomTile_badge': true,
|
'mx_RoomTile_badge': true,
|
||||||
|
|
|
@ -135,8 +135,8 @@ var SearchableEntityList = React.createClass({
|
||||||
<form onSubmit={this.onQuerySubmit} autoComplete="off">
|
<form onSubmit={this.onQuerySubmit} autoComplete="off">
|
||||||
<input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
|
<input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
|
||||||
onChange={this.onQueryChanged} value={this.state.query}
|
onChange={this.onQueryChanged} value={this.state.query}
|
||||||
onFocus= {() => { this.setState({ focused: true }) }}
|
onFocus= {() => { this.setState({ focused: true }); }}
|
||||||
onBlur= {() => { this.setState({ focused: false }) }}
|
onBlur= {() => { this.setState({ focused: false }); }}
|
||||||
placeholder={this.props.searchPlaceholderText} />
|
placeholder={this.props.searchPlaceholderText} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,7 +45,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var cancelButton;
|
var cancelButton;
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancelButton = <AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </AccessibleButton>
|
cancelButton = <AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var showRhsButton;
|
var showRhsButton;
|
||||||
|
@ -71,4 +71,3 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
avatarUrl: this.props.initialAvatarUrl,
|
avatarUrl: this.props.initialAvatarUrl,
|
||||||
phase: this.Phases.Display,
|
phase: this.Phases.Display,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
@ -120,7 +120,7 @@ module.exports = React.createClass({
|
||||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
|
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
|
||||||
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
|
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
|
||||||
name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />
|
name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var uploadSection;
|
var uploadSection;
|
||||||
|
|
|
@ -60,7 +60,7 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
phase: this.Phases.Edit
|
phase: this.Phases.Edit
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
changePassword: function(old_password, new_password) {
|
changePassword: function(old_password, new_password) {
|
||||||
|
@ -106,7 +106,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
var rowClassName = this.props.rowClassName;
|
var rowClassName = this.props.rowClassName;
|
||||||
var rowLabelClassName = this.props.rowLabelClassName;
|
var rowLabelClassName = this.props.rowLabelClassName;
|
||||||
var rowInputClassName = this.props.rowInputClassName
|
var rowInputClassName = this.props.rowInputClassName;
|
||||||
var buttonClassName = this.props.buttonClassName;
|
var buttonClassName = this.props.buttonClassName;
|
||||||
|
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default class DevicesPanel extends React.Component {
|
||||||
const removed_id = device.device_id;
|
const removed_id = device.device_id;
|
||||||
this.setState((state, props) => {
|
this.setState((state, props) => {
|
||||||
const newDevices = state.devices.filter(
|
const newDevices = state.devices.filter(
|
||||||
d => { return d.device_id != removed_id }
|
d => { return d.device_id != removed_id; }
|
||||||
);
|
);
|
||||||
return { devices: newDevices };
|
return { devices: newDevices };
|
||||||
});
|
});
|
||||||
|
@ -98,7 +98,7 @@ export default class DevicesPanel extends React.Component {
|
||||||
var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
||||||
return (
|
return (
|
||||||
<DevicesPanelEntry key={device.device_id} device={device}
|
<DevicesPanelEntry key={device.device_id} device={device}
|
||||||
onDeleted={()=>{this._onDeviceDeleted(device)}} />
|
onDeleted={()=>{this._onDeviceDeleted(device);}} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import q from 'q';
|
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import DateUtils from '../../../DateUtils';
|
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
export default class DevicesPanelEntry extends React.Component {
|
export default class DevicesPanelEntry extends React.Component {
|
||||||
|
@ -61,7 +58,7 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
if (this._unmounted) { return; }
|
if (this._unmounted) { return; }
|
||||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||||
// doesn't look like an interactive-auth failure
|
// doesn't look like an interactive-auth failure
|
||||||
throw e;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pop up an interactive auth dialog
|
// pop up an interactive auth dialog
|
||||||
|
@ -121,7 +118,7 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
|
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if (this.state.deleteError) {
|
if (this.state.deleteError) {
|
||||||
deleteButton = <div className="error">{this.state.deleteError}</div>
|
deleteButton = <div className="error">{this.state.deleteError}</div>;
|
||||||
} else {
|
} else {
|
||||||
deleteButton = (
|
deleteButton = (
|
||||||
<div className="mx_textButton"
|
<div className="mx_textButton"
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
var Notifier = require("../../../Notifier");
|
var Notifier = require("../../../Notifier");
|
||||||
var sdk = require('../../../index');
|
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
|
|
@ -45,7 +45,7 @@ function createRoom(opts) {
|
||||||
Modal.createDialog(NeedToRegisterDialog, {
|
Modal.createDialog(NeedToRegisterDialog, {
|
||||||
title: "Please Register",
|
title: "Please Register",
|
||||||
description: "Guest users can't create new rooms. Please register to create room and start a chat."
|
description: "Guest users can't create new rooms. Please register to create room and start a chat."
|
||||||
})
|
});
|
||||||
}, 0);
|
}, 0);
|
||||||
return q(null);
|
return q(null);
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ function createRoom(opts) {
|
||||||
|
|
||||||
let modal;
|
let modal;
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner')
|
modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
let roomId;
|
let roomId;
|
||||||
|
|
|
@ -39,8 +39,11 @@ class MatrixDispatcher extends flux.Dispatcher {
|
||||||
setTimeout(super.dispatch.bind(this, payload), 0);
|
setTimeout(super.dispatch.bind(this, payload), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// XXX this is a big anti-pattern, and makes testing hard. Because dispatches
|
||||||
|
// happen asynchronously, it is possible for actions dispatched in one thread
|
||||||
|
// to arrive in another, with *hilarious* consequences.
|
||||||
if (global.mxDispatcher === undefined) {
|
if (global.mxDispatcher === undefined) {
|
||||||
global.mxDispatcher = new MatrixDispatcher();
|
global.mxDispatcher = new MatrixDispatcher();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,4 +23,4 @@ module.exports = function(dest, src) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dest;
|
return dest;
|
||||||
}
|
};
|
||||||
|
|
24
src/index.js
24
src/index.js
|
@ -28,3 +28,27 @@ module.exports.getComponent = function(componentName) {
|
||||||
return Skinner.getComponent(componentName);
|
return Skinner.getComponent(componentName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* hacky functions for megolm import/export until we give it a UI */
|
||||||
|
import * as MegolmExportEncryption from './utils/MegolmExportEncryption';
|
||||||
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
|
window.exportKeys = function(password) {
|
||||||
|
return MatrixClientPeg.get().exportRoomKeys().then((k) => {
|
||||||
|
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||||
|
JSON.stringify(k), password
|
||||||
|
);
|
||||||
|
}).then((f) => {
|
||||||
|
console.log(new TextDecoder().decode(new Uint8Array(f)));
|
||||||
|
}).done();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.importKeys = function(password, data) {
|
||||||
|
const arrayBuffer = new TextEncoder().encode(data).buffer;
|
||||||
|
return MegolmExportEncryption.decryptMegolmKeyFile(
|
||||||
|
arrayBuffer, password
|
||||||
|
).then((j) => {
|
||||||
|
const k = JSON.parse(j);
|
||||||
|
return MatrixClientPeg.get().importRoomKeys(k);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
319
src/utils/MegolmExportEncryption.js
Normal file
319
src/utils/MegolmExportEncryption.js
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// polyfill textencoder if necessary
|
||||||
|
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
||||||
|
let TextEncoder = window.TextEncoder;
|
||||||
|
if (!TextEncoder) {
|
||||||
|
TextEncoder = TextEncodingUtf8.TextEncoder;
|
||||||
|
}
|
||||||
|
let TextDecoder = window.TextDecoder;
|
||||||
|
if (!TextDecoder) {
|
||||||
|
TextDecoder = TextEncodingUtf8.TextDecoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a megolm key file
|
||||||
|
*
|
||||||
|
* @param {ArrayBuffer} file
|
||||||
|
* @param {String} password
|
||||||
|
* @return {Promise<String>} promise for decrypted output
|
||||||
|
*/
|
||||||
|
export function decryptMegolmKeyFile(data, password) {
|
||||||
|
const body = unpackMegolmKeyFile(data);
|
||||||
|
|
||||||
|
// check we have a version byte
|
||||||
|
if (body.length < 1) {
|
||||||
|
throw new Error('Invalid file: too short');
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = body[0];
|
||||||
|
if (version !== 1) {
|
||||||
|
throw new Error('Unsupported version');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ciphertextLength = body.length-(1+16+16+4+32);
|
||||||
|
if (body.length < 0) {
|
||||||
|
throw new Error('Invalid file: too short');
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = body.subarray(1, 1+16);
|
||||||
|
const iv = body.subarray(17, 17+16);
|
||||||
|
const iterations = body[33] << 24 | body[34] << 16 | body[35] << 8 | body[36];
|
||||||
|
const ciphertext = body.subarray(37, 37+ciphertextLength);
|
||||||
|
const hmac = body.subarray(-32);
|
||||||
|
|
||||||
|
return deriveKeys(salt, iterations, password).then((keys) => {
|
||||||
|
const [aes_key, hmac_key] = keys;
|
||||||
|
|
||||||
|
const toVerify = body.subarray(0, -32);
|
||||||
|
return subtleCrypto.verify(
|
||||||
|
{name: 'HMAC'},
|
||||||
|
hmac_key,
|
||||||
|
hmac,
|
||||||
|
toVerify,
|
||||||
|
).then((isValid) => {
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error('Authentication check failed: incorrect password?');
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtleCrypto.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: iv,
|
||||||
|
length: 64,
|
||||||
|
},
|
||||||
|
aes_key,
|
||||||
|
ciphertext,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}).then((plaintext) => {
|
||||||
|
return new TextDecoder().decode(new Uint8Array(plaintext));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt a megolm key file
|
||||||
|
*
|
||||||
|
* @param {String} data
|
||||||
|
* @param {String} password
|
||||||
|
* @param {Object=} options
|
||||||
|
* @param {Nunber=} options.kdf_rounds Number of iterations to perform of the
|
||||||
|
* key-derivation function.
|
||||||
|
* @return {Promise<ArrayBuffer>} promise for encrypted output
|
||||||
|
*/
|
||||||
|
export function encryptMegolmKeyFile(data, password, options) {
|
||||||
|
options = options || {};
|
||||||
|
const kdf_rounds = options.kdf_rounds || 100000;
|
||||||
|
|
||||||
|
const salt = new Uint8Array(16);
|
||||||
|
window.crypto.getRandomValues(salt);
|
||||||
|
|
||||||
|
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
|
||||||
|
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||||
|
// of a single bit of salt is a price we have to pay.
|
||||||
|
salt[9] &= 0x7f;
|
||||||
|
|
||||||
|
const iv = new Uint8Array(16);
|
||||||
|
window.crypto.getRandomValues(iv);
|
||||||
|
|
||||||
|
return deriveKeys(salt, kdf_rounds, password).then((keys) => {
|
||||||
|
const [aes_key, hmac_key] = keys;
|
||||||
|
|
||||||
|
return subtleCrypto.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: iv,
|
||||||
|
length: 64,
|
||||||
|
},
|
||||||
|
aes_key,
|
||||||
|
new TextEncoder().encode(data),
|
||||||
|
).then((ciphertext) => {
|
||||||
|
const cipherArray = new Uint8Array(ciphertext);
|
||||||
|
const bodyLength = (1+salt.length+iv.length+4+cipherArray.length+32);
|
||||||
|
const resultBuffer = new Uint8Array(bodyLength);
|
||||||
|
let idx = 0;
|
||||||
|
resultBuffer[idx++] = 1; // version
|
||||||
|
resultBuffer.set(salt, idx); idx += salt.length;
|
||||||
|
resultBuffer.set(iv, idx); idx += iv.length;
|
||||||
|
resultBuffer[idx++] = kdf_rounds >> 24;
|
||||||
|
resultBuffer[idx++] = (kdf_rounds >> 16) & 0xff;
|
||||||
|
resultBuffer[idx++] = (kdf_rounds >> 8) & 0xff;
|
||||||
|
resultBuffer[idx++] = kdf_rounds & 0xff;
|
||||||
|
resultBuffer.set(cipherArray, idx); idx += cipherArray.length;
|
||||||
|
|
||||||
|
const toSign = resultBuffer.subarray(0, idx);
|
||||||
|
|
||||||
|
return subtleCrypto.sign(
|
||||||
|
{name: 'HMAC'},
|
||||||
|
hmac_key,
|
||||||
|
toSign,
|
||||||
|
).then((hmac) => {
|
||||||
|
hmac = new Uint8Array(hmac);
|
||||||
|
resultBuffer.set(hmac, idx);
|
||||||
|
return packMegolmKeyFile(resultBuffer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the AES and HMAC-SHA-256 keys for the file
|
||||||
|
*
|
||||||
|
* @param {Unit8Array} salt salt for pbkdf
|
||||||
|
* @param {Number} iterations number of pbkdf iterations
|
||||||
|
* @param {String} password password
|
||||||
|
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key]
|
||||||
|
*/
|
||||||
|
function deriveKeys(salt, iterations, password) {
|
||||||
|
return subtleCrypto.importKey(
|
||||||
|
'raw',
|
||||||
|
new TextEncoder().encode(password),
|
||||||
|
{name: 'PBKDF2'},
|
||||||
|
false,
|
||||||
|
['deriveBits']
|
||||||
|
).then((key) => {
|
||||||
|
return subtleCrypto.deriveBits(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: salt,
|
||||||
|
iterations: iterations,
|
||||||
|
hash: 'SHA-512',
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
512
|
||||||
|
);
|
||||||
|
}).then((keybits) => {
|
||||||
|
const aes_key = keybits.slice(0, 32);
|
||||||
|
const hmac_key = keybits.slice(32);
|
||||||
|
|
||||||
|
const aes_prom = subtleCrypto.importKey(
|
||||||
|
'raw',
|
||||||
|
aes_key,
|
||||||
|
{name: 'AES-CTR'},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
const hmac_prom = subtleCrypto.importKey(
|
||||||
|
'raw',
|
||||||
|
hmac_key,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: {name: 'SHA-256'},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['sign', 'verify']
|
||||||
|
);
|
||||||
|
return Promise.all([aes_prom, hmac_prom]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEADER_LINE = '-----BEGIN MEGOLM SESSION DATA-----';
|
||||||
|
const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unbase64 an ascii-armoured megolm key file
|
||||||
|
*
|
||||||
|
* Strips the header and trailer lines, and unbase64s the content
|
||||||
|
*
|
||||||
|
* @param {ArrayBuffer} data input file
|
||||||
|
* @return {Uint8Array} unbase64ed content
|
||||||
|
*/
|
||||||
|
function unpackMegolmKeyFile(data) {
|
||||||
|
// parse the file as a great big String. This should be safe, because there
|
||||||
|
// should be no non-ASCII characters, and it means that we can do string
|
||||||
|
// comparisons to find the header and footer, and feed it into window.atob.
|
||||||
|
const fileStr = new TextDecoder().decode(new Uint8Array(data));
|
||||||
|
|
||||||
|
// look for the start line
|
||||||
|
let lineStart = 0;
|
||||||
|
while (1) {
|
||||||
|
const lineEnd = fileStr.indexOf('\n', lineStart);
|
||||||
|
if (lineEnd < 0) {
|
||||||
|
throw new Error('Header line not found');
|
||||||
|
}
|
||||||
|
const line = fileStr.slice(lineStart, lineEnd).trim();
|
||||||
|
|
||||||
|
// start the next line after the newline
|
||||||
|
lineStart = lineEnd+1;
|
||||||
|
|
||||||
|
if (line === HEADER_LINE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataStart = lineStart;
|
||||||
|
|
||||||
|
// look for the end line
|
||||||
|
while (1) {
|
||||||
|
const lineEnd = fileStr.indexOf('\n', lineStart);
|
||||||
|
const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd)
|
||||||
|
.trim();
|
||||||
|
if (line === TRAILER_LINE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineEnd < 0) {
|
||||||
|
throw new Error('Trailer line not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the next line after the newline
|
||||||
|
lineStart = lineEnd+1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataEnd = lineStart;
|
||||||
|
return decodeBase64(fileStr.slice(dataStart, dataEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ascii-armour a megolm key file
|
||||||
|
*
|
||||||
|
* base64s the content, and adds header and trailer lines
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} data raw data
|
||||||
|
* @return {ArrayBuffer} formatted file
|
||||||
|
*/
|
||||||
|
function packMegolmKeyFile(data) {
|
||||||
|
// we split into lines before base64ing, because encodeBase64 doesn't deal
|
||||||
|
// terribly well with large arrays.
|
||||||
|
const LINE_LENGTH = (72 * 4 / 3);
|
||||||
|
const nLines = Math.ceil(data.length / LINE_LENGTH);
|
||||||
|
const lines = new Array(nLines + 3);
|
||||||
|
lines[0] = HEADER_LINE;
|
||||||
|
let o = 0;
|
||||||
|
let i;
|
||||||
|
for (i = 1; i <= nLines; i++) {
|
||||||
|
lines[i] = encodeBase64(data.subarray(o, o+LINE_LENGTH));
|
||||||
|
o += LINE_LENGTH;
|
||||||
|
}
|
||||||
|
lines[i++] = TRAILER_LINE;
|
||||||
|
lines[i] = '';
|
||||||
|
return (new TextEncoder().encode(lines.join('\n'))).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a typed array of uint8 as base64.
|
||||||
|
* @param {Uint8Array} uint8Array The data to encode.
|
||||||
|
* @return {string} The base64.
|
||||||
|
*/
|
||||||
|
function encodeBase64(uint8Array) {
|
||||||
|
// Misinterpt the Uint8Array as Latin-1.
|
||||||
|
// window.btoa expects a unicode string with codepoints in the range 0-255.
|
||||||
|
var latin1String = String.fromCharCode.apply(null, uint8Array);
|
||||||
|
// Use the builtin base64 encoder.
|
||||||
|
return window.btoa(latin1String);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a base64 string to a typed array of uint8.
|
||||||
|
* @param {string} base64 The base64 to decode.
|
||||||
|
* @return {Uint8Array} The decoded data.
|
||||||
|
*/
|
||||||
|
function decodeBase64(base64) {
|
||||||
|
// window.atob returns a unicode string with codepoints in the range 0-255.
|
||||||
|
var latin1String = window.atob(base64);
|
||||||
|
// Encode the string as a Uint8Array
|
||||||
|
var uint8Array = new Uint8Array(latin1String.length);
|
||||||
|
for (var i = 0; i < latin1String.length; i++) {
|
||||||
|
uint8Array[i] = latin1String.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return uint8Array;
|
||||||
|
}
|
|
@ -36,4 +36,4 @@ export default function(WrappedComponent) {
|
||||||
return <WrappedComponent {...this.props} matrixClient={this.context.matrixClient} />;
|
return <WrappedComponent {...this.props} matrixClient={this.context.matrixClient} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
5
test/.eslintrc.js
Normal file
5
test/.eslintrc.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
mocha: true,
|
||||||
|
},
|
||||||
|
}
|
|
@ -158,4 +158,85 @@ describe('MessageComposerInput', () => {
|
||||||
expect(['__', '**']).toContain(spy.args[0][1]);
|
expect(['__', '**']).toContain(spy.args[0][1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not entity-encode " in Markdown mode', () => {
|
||||||
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('"');
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.calledOnce).toEqual(true);
|
||||||
|
expect(spy.args[0][1]).toEqual('"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape characters without other markup in Markdown mode', () => {
|
||||||
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('\\*escaped\\*');
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.calledOnce).toEqual(true);
|
||||||
|
expect(spy.args[0][1]).toEqual('*escaped*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape characters with other markup in Markdown mode', () => {
|
||||||
|
const spy = sinon.spy(client, 'sendHtmlMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('\\*escaped\\* *italic*');
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.calledOnce).toEqual(true);
|
||||||
|
expect(spy.args[0][1]).toEqual('\\*escaped\\* *italic*');
|
||||||
|
expect(spy.args[0][2]).toEqual('*escaped* <em>italic</em>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not convert -_- into a horizontal rule in Markdown mode', () => {
|
||||||
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('-_-');
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.calledOnce).toEqual(true);
|
||||||
|
expect(spy.args[0][1]).toEqual('-_-');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not strip <del> tags in Markdown mode', () => {
|
||||||
|
const spy = sinon.spy(client, 'sendHtmlMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('<del>striked-out</del>');
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.calledOnce).toEqual(true);
|
||||||
|
expect(spy.args[0][1]).toEqual('<del>striked-out</del>');
|
||||||
|
expect(spy.args[0][2]).toEqual('<del>striked-out</del>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not strike-through ~~~ in Markdown mode', () => {
|
||||||
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('~~~striked-out~~~');
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.calledOnce).toEqual(true);
|
||||||
|
expect(spy.args[0][1]).toEqual('~~~striked-out~~~');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mark single unmarkedup paragraphs as HTML in Markdown mode', () => {
|
||||||
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.calledOnce).toEqual(true);
|
||||||
|
expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mark two unmarkedup paragraphs as HTML in Markdown mode', () => {
|
||||||
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.calledOnce).toEqual(true);
|
||||||
|
expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue