Merge branch 'develop' into luke/fix-join-part-collapsing-causality

Conflicts:
	src/components/structures/MessagePanel.js
This commit is contained in:
Luke Barnard 2017-01-24 14:22:18 +00:00
commit bd106968ce
124 changed files with 1843 additions and 657 deletions

117
.eslintrc
View File

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

View File

@ -1,3 +1,9 @@
language: node_js language: node_js
node_js: node_js:
- node # Latest stable version of nodejs. - node # Latest stable version of nodejs.
install:
- npm install
- (cd node_modules/matrix-js-sdk && npm install)
script:
- npm run test
- ./.travis-test-riot.sh

View File

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

View File

@ -12,11 +12,14 @@ set -x
# install the other dependencies # install the other dependencies
npm install npm install
# we may be using a dev branch of js-sdk in which case we need to build it
(cd node_modules/matrix-js-sdk && npm install)
# run the mocha tests # run the mocha tests
npm run test npm run test
# run eslint # run eslint
npm run lint -- -f checkstyle -o eslint.xml || true npm run lintall -- -f checkstyle -o eslint.xml || true
# delete the old tarball, if it exists # delete the old tarball, if it exists
rm -f matrix-react-sdk-*.tgz rm -f matrix-react-sdk-*.tgz

View File

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

View File

@ -21,7 +21,7 @@ var MatrixClientPeg = require("./MatrixClientPeg");
* optionally, the identity servers. * optionally, the identity servers.
* *
* This involves getting an email token from the identity server to "prove" that * This involves getting an email token from the identity server to "prove" that
* the client owns the given email address, which is then passed to the * the client owns the given email address, which is then passed to the
* add threepid API on the homeserver. * add threepid API on the homeserver.
*/ */
class AddThreepid { class AddThreepid {

View File

@ -49,12 +49,12 @@ module.exports = {
}, },
defaultAvatarUrlForString: function(s) { defaultAvatarUrlForString: function(s) {
var images = [ '76cfa6', '50e2c2', 'f4c371' ]; var images = ['76cfa6', '50e2c2', 'f4c371'];
var total = 0; var total = 0;
for (var i = 0; i < s.length; ++i) { for (var i = 0; i < s.length; ++i) {
total += s.charCodeAt(i); total += s.charCodeAt(i);
} }
return 'img/' + images[total % images.length] + '.png'; return 'img/' + images[total % images.length] + '.png';
} }
} };

View File

@ -41,7 +41,7 @@ export default class BasePlatform {
* Returns true if the platform supports displaying * Returns true if the platform supports displaying
* notifications, otherwise false. * notifications, otherwise false.
*/ */
supportsNotifications() : boolean { supportsNotifications(): boolean {
return false; return false;
} }
@ -49,7 +49,7 @@ export default class BasePlatform {
* Returns true if the application currently has permission * Returns true if the application currently has permission
* to display notifications. Otherwise false. * to display notifications. Otherwise false.
*/ */
maySendNotifications() : boolean { maySendNotifications(): boolean {
return false; return false;
} }
@ -60,7 +60,7 @@ export default class BasePlatform {
* that is 'granted' if the user allowed the request or * that is 'granted' if the user allowed the request or
* 'denied' otherwise. * 'denied' otherwise.
*/ */
requestNotificationPermission() : Promise<string> { requestNotificationPermission(): Promise<string> {
} }
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {

View File

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

View File

@ -256,7 +256,7 @@ function uploadFile(matrixClient, roomId, file) {
}); });
}); });
} else { } else {
const basePromise = matrixClient.uploadContent(file); const basePromise = matrixClient.uploadContent(file);
const promise1 = basePromise.then(function(url) { const promise1 = basePromise.then(function(url) {
// If the attachment isn't encrypted then include the URL directly. // If the attachment isn't encrypted then include the URL directly.
return {"url": url}; return {"url": url};

View File

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

View File

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

View File

@ -91,16 +91,16 @@ var sanitizeHtmlParams = {
], ],
allowedAttributes: { allowedAttributes: {
// custom ones first: // custom ones first:
font: [ 'color' ], // custom to matrix font: ['color'], // custom to matrix
a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
// We don't currently allow img itself by default, but this // We don't currently allow img itself by default, but this
// would make sense if we did // would make sense if we did
img: [ 'src' ], img: ['src'],
}, },
// Lots of these won't come up by default because we don't allow them // Lots of these won't come up by default because we don't allow them
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ], selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit // URL schemes we permit
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
// DO NOT USE. sanitize-html allows all URL starting with '//' // DO NOT USE. sanitize-html allows all URL starting with '//'
// so this will always allow links to whatever scheme the // so this will always allow links to whatever scheme the

View File

@ -53,5 +53,5 @@ module.exports = {
return Math.floor(heightMulti * fullHeight); return Math.floor(heightMulti * fullHeight);
} }
}, },
} };

View File

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

View File

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

View File

@ -14,20 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import marked from 'marked'; import commonmark from 'commonmark';
// marked only applies the default options on the high
// level marked() interface, so we do it here.
const marked_options = Object.assign({}, marked.defaults, {
gfm: true,
tables: true,
breaks: true,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false,
xhtml: true, // return self closing tags (ie. <br /> not <br>)
});
/** /**
* Class that wraps marked, adding the ability to see whether * Class that wraps marked, adding the ability to see whether
@ -36,16 +23,9 @@ const marked_options = Object.assign({}, marked.defaults, {
*/ */
export default class Markdown { export default class Markdown {
constructor(input) { constructor(input) {
const lexer = new marked.Lexer(marked_options); this.input = input;
this.tokens = lexer.lex(input); this.parser = new commonmark.Parser();
} this.renderer = new commonmark.HtmlRenderer({safe: false});
_copyTokens() {
// copy tokens (the parser modifies its input arg)
const tokens_copy = this.tokens.slice();
// it also has a 'links' property, because this is javascript
// and why wouldn't you have an array that also has properties?
return Object.assign(tokens_copy, this.tokens);
} }
isPlainText() { isPlainText() {
@ -64,65 +44,81 @@ export default class Markdown {
is_plain = false; is_plain = false;
} }
const dummy_renderer = {}; const dummy_renderer = new commonmark.HtmlRenderer();
for (const k of Object.keys(marked.Renderer.prototype)) { for (const k of Object.keys(commonmark.HtmlRenderer.prototype)) {
dummy_renderer[k] = setNotPlain; dummy_renderer[k] = setNotPlain;
} }
// text and paragraph are just text // text and paragraph are just text
dummy_renderer.text = function(t){return t;} dummy_renderer.text = function(t) { return t; };
dummy_renderer.paragraph = function(t){return t;} dummy_renderer.softbreak = function(t) { return t; };
dummy_renderer.paragraph = function(t) { return t; };
// ignore links where text is just the url: const dummy_parser = new commonmark.Parser();
// this ignores plain URLs that markdown has dummy_renderer.render(dummy_parser.parse(this.input));
// detected whilst preserving markdown syntax links
dummy_renderer.link = function(href, title, text) {
if (text != href) {
is_plain = false;
}
}
const dummy_options = Object.assign({}, marked_options, {
renderer: dummy_renderer,
});
const dummy_parser = new marked.Parser(dummy_options);
dummy_parser.parse(this._copyTokens());
return is_plain; return is_plain;
} }
toHTML() { toHTML() {
const real_renderer = new marked.Renderer(); const real_paragraph = this.renderer.paragraph;
real_renderer.link = function(href, title, text) {
// prevent marked from turning plain URLs
// into links, because its algorithm is fairly
// poor. Let's send plain URLs rather than
// badly linkified ones (the linkifier Vector
// uses on message display is way better, eg.
// handles URLs with closing parens at the end).
if (text == href) {
return href;
}
return marked.Renderer.prototype.link.apply(this, arguments);
}
real_renderer.paragraph = (text) => { this.renderer.paragraph = function(node, entering) {
// The tokens at the top level are the 'blocks', so if we // If there is only one top level node, just return the
// have more than one, there are multiple 'paragraphs'.
// If there is only one top level token, just return the
// bare text: it's a single line of text and so should be // bare text: it's a single line of text and so should be
// 'inline', rather than necessarily wrapped in its own // 'inline', rather than unnecessarily wrapped in its own
// p tag. If, however, we have multiple tokens, each gets // p tag. If, however, we have multiple nodes, each gets
// its own p tag to keep them as separate paragraphs. // its own p tag to keep them as separate paragraphs.
if (this.tokens.length == 1) { var par = node;
return text; while (par.parent) {
par = par.parent;
} }
return '<p>' + text + '</p>'; if (par.firstChild != par.lastChild) {
} real_paragraph.call(this, node, entering);
}
};
const real_options = Object.assign({}, marked_options, { var parsed = this.parser.parse(this.input);
renderer: real_renderer, var rendered = this.renderer.render(parsed);
});
const real_parser = new marked.Parser(real_options); this.renderer.paragraph = real_paragraph;
return real_parser.parse(this._copyTokens());
return rendered;
}
toPlaintext() {
const real_paragraph = this.renderer.paragraph;
// The default `out` function only sends the input through an XML
// escaping function, which causes messages to be entity encoded,
// which we don't want in this case.
this.renderer.out = function(s) {
// The `lit` function adds a string literal to the output buffer.
this.lit(s);
};
this.renderer.paragraph = function(node, entering) {
// If there is only one top level node, just return the
// bare text: it's a single line of text and so should be
// 'inline', rather than unnecessarily wrapped in its own
// p tag. If, however, we have multiple nodes, each gets
// its own p tag to keep them as separate paragraphs.
var par = node;
while (par.parent) {
node = par;
par = par.parent;
}
if (node != par.lastChild) {
if (!entering) {
this.lit('\n\n');
}
}
};
var parsed = this.parser.parse(this.input);
var rendered = this.renderer.render(parsed);
this.renderer.paragraph = real_paragraph;
return rendered;
} }
} }

View File

@ -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",
@ -35,21 +84,47 @@ module.exports = {
return container; return container;
}, },
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>

View File

@ -53,7 +53,7 @@ var Notifier = {
if (!msg) return; if (!msg) return;
var title; var title;
if (!ev.sender || room.name == ev.sender.name) { if (!ev.sender || room.name == ev.sender.name) {
title = room.name; title = room.name;
// notificationMessageForEvent includes sender, // notificationMessageForEvent includes sender,
// but we already have the sender here // but we already have the sender here
@ -88,7 +88,7 @@ var Notifier = {
if (e) { if (e) {
e.load(); e.load();
e.play(); e.play();
}; }
}, },
start: function() { start: function() {

View File

@ -64,7 +64,7 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
} else if (itemDelta[item] === -1) { } else if (itemDelta[item] === -1) {
results.push({ place: "del", key: muxedKey, val: item }); results.push({ place: "del", key: muxedKey, val: item });
} else { } else {
// itemDelta of 0 means it was unchanged between before/after // itemDelta of 0 means it was unchanged between before/after
} }
}); });
break; break;

View File

@ -111,7 +111,7 @@ class Presence {
this.timer = setTimeout(function() { this.timer = setTimeout(function() {
self._onUnavailableTimerFire(); self._onUnavailableTimerFire();
}, UNAVAILABLE_TIME_MS); }, UNAVAILABLE_TIME_MS);
} }
} }
module.exports = new Presence(); module.exports = new Presence();

View File

@ -12,7 +12,7 @@ import {
SelectionState, SelectionState,
Entity, Entity,
} from 'draft-js'; } from 'draft-js';
import * as sdk from './index'; import * as sdk from './index';
import * as emojione from 'emojione'; import * as emojione from 'emojione';
import {stateToHTML} from 'draft-js-export-html'; import {stateToHTML} from 'draft-js-export-html';
import {SelectionRange} from "./autocomplete/Autocompleter"; import {SelectionRange} from "./autocomplete/Autocompleter";
@ -109,7 +109,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
return <span className="mx_UserPill">{avatar}{props.children}</span>; return <span className="mx_UserPill">{avatar}{props.children}</span>;
} }
}; };
let roomDecorator = { let roomDecorator = {
strategy: (contentBlock, callback) => { strategy: (contentBlock, callback) => {
findWithRegex(ROOM_REGEX, contentBlock, callback); findWithRegex(ROOM_REGEX, contentBlock, callback);

View File

@ -26,7 +26,7 @@ function tsOfNewestEvent(room) {
} }
function mostRecentActivityFirst(roomList) { function mostRecentActivityFirst(roomList) {
return roomList.sort(function(a,b) { return roomList.sort(function(a, b) {
return tsOfNewestEvent(b) - tsOfNewestEvent(a); return tsOfNewestEvent(b) - tsOfNewestEvent(a);
}); });
} }

View File

@ -146,7 +146,7 @@ function isRuleForRoom(roomId, rule) {
} }
const cond = rule.conditions[0]; const cond = rule.conditions[0];
if ( if (
cond.kind == 'event_match' && cond.kind == 'event_match' &&
cond.key == 'room_id' && cond.key == 'room_id' &&
cond.pattern == roomId cond.pattern == roomId
) { ) {

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ class Command {
} }
getUsage() { getUsage() {
return "Usage: " + this.getCommandWithArgs() return "Usage: " + this.getCommandWithArgs();
} }
} }
@ -84,7 +84,7 @@ var commands = {
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
if (matches) { if (matches) {
Tinter.tint(matches[1], matches[4]); Tinter.tint(matches[1], matches[4]);
var colorScheme = {} var colorScheme = {};
colorScheme.primary_color = matches[1]; colorScheme.primary_color = matches[1];
if (matches[4]) { if (matches[4]) {
colorScheme.secondary_color = matches[4]; colorScheme.secondary_color = matches[4];
@ -288,7 +288,7 @@ var commands = {
// helpful aliases // helpful aliases
var aliases = { var aliases = {
j: "join" j: "join"
} };
module.exports = { module.exports = {
/** /**
@ -331,9 +331,9 @@ module.exports = {
// Return all the commands plus /me and /markdown which aren't handled like normal commands // Return all the commands plus /me and /markdown which aren't handled like normal commands
var cmds = Object.keys(commands).sort().map(function(cmdKey) { var cmds = Object.keys(commands).sort().map(function(cmdKey) {
return commands[cmdKey]; return commands[cmdKey];
}) });
cmds.push(new Command("me", "<action>", function(){})); cmds.push(new Command("me", "<action>", function() {}));
cmds.push(new Command("markdown", "<on|off>", function(){})); cmds.push(new Command("markdown", "<on|off>", function() {}));
return cmds; return cmds;
} }

View File

@ -112,7 +112,7 @@ class TabComplete {
return; return;
} }
// ES6 destructuring; ignore first element (the complete match) // ES6 destructuring; ignore first element (the complete match)
var [ , boundaryGroup, partialGroup] = res; var [, boundaryGroup, partialGroup] = res;
if (partialGroup.length === 0 && passive) { if (partialGroup.length === 0 && passive) {
return; return;
@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -46,4 +46,4 @@ module.exports = {
return names.join(', ') + ' and ' + lastPerson + ' are typing'; return names.join(', ') + ' and ' + lastPerson + ' are typing';
} }
} }
} };

View 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>
);
},
});

View File

@ -26,7 +26,7 @@ export default class AutocompleteProvider {
} }
commandRegex.lastIndex = 0; commandRegex.lastIndex = 0;
let match; let match;
while ((match = commandRegex.exec(query)) != null) { while ((match = commandRegex.exec(query)) != null) {
let matchStart = match.index, let matchStart = match.index,

View File

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

View File

@ -13,7 +13,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
constructor() { constructor() {
super(DDG_REGEX); super(DDG_REGEX);
} }
static getQueryUri(query: String) { static getQueryUri(query: String) {
return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}` return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;

View File

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

View File

@ -52,12 +52,12 @@ export default class RoomProvider extends AutocompleteProvider {
getName() { getName() {
return '💬 Rooms'; return '💬 Rooms';
} }
static getInstance() { static getInstance() {
if (instance == null) { if (instance == null) {
instance = new RoomProvider(); instance = new RoomProvider();
} }
return instance; return instance;
} }

View File

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

View File

@ -47,7 +47,7 @@ module.exports = {
return container; return container;
}, },
createMenu: function (Element, props) { createMenu: function(Element, props) {
var self = this; var self = this;
var closeMenu = function() { var closeMenu = function() {
@ -67,7 +67,7 @@ module.exports = {
chevronOffset.top = props.chevronOffset; chevronOffset.top = props.chevronOffset;
} }
// To overide the deafult chevron colour, if it's been set // To override the default chevron colour, if it's been set
var chevronCSS = ""; var chevronCSS = "";
if (props.menuColour) { if (props.menuColour) {
chevronCSS = ` chevronCSS = `
@ -78,15 +78,15 @@ module.exports = {
.mx_ContextualMenu_chevron_right:after { .mx_ContextualMenu_chevron_right:after {
border-left-color: ${props.menuColour}; border-left-color: ${props.menuColour};
} }
` `;
} }
var chevron = null; var chevron = null;
if (props.left) { if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div> chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
position.left = props.left; position.left = props.left;
} else { } else {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div> chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
position.right = props.right; position.right = props.right;
} }

View File

@ -118,7 +118,7 @@ module.exports = React.createClass({
var self = this; var self = this;
deferred.then(function (resp) { deferred.then(function(resp) {
self.setState({ self.setState({
phase: self.phases.CREATED, phase: self.phases.CREATED,
}); });
@ -210,7 +210,7 @@ module.exports = React.createClass({
onAliasChanged: function(alias) { onAliasChanged: function(alias) {
this.setState({ this.setState({
alias: alias alias: alias
}) });
}, },
onEncryptChanged: function(ev) { onEncryptChanged: function(ev) {

View File

@ -35,7 +35,7 @@ var FilePanel = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
timelineSet: null, timelineSet: null,
} };
}, },
componentWillMount: function() { componentWillMount: function() {

View File

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

View File

@ -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() {
@ -259,6 +259,8 @@ module.exports = React.createClass({
}, },
onAction: function(payload) { onAction: function(payload) {
console.log("onAction: "+payload.action);
var roomIndexDelta = 1; var roomIndexDelta = 1;
var self = this; var self = this;
@ -456,6 +458,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 +591,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 +736,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) {
@ -949,8 +1008,8 @@ module.exports = React.createClass({
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
var LoggedInView = sdk.getComponent('structures.LoggedInView'); var LoggedInView = sdk.getComponent('structures.LoggedInView');
// console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen + console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
// "; logged_in="+this.state.logged_in+"; ready="+this.state.ready); "; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
if (this.state.loading) { if (this.state.loading) {
var Spinner = sdk.getComponent('elements.Spinner'); var Spinner = sdk.getComponent('elements.Spinner');
@ -979,7 +1038,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 +1062,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}

View File

@ -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;
@ -281,7 +281,7 @@ module.exports = React.createClass({
var isMembershipChange = (e) => var isMembershipChange = (e) =>
e.getType() === 'm.room.member' e.getType() === 'm.room.member'
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership); && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
for (i = 0; i < this.props.events.length; i++) { for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i]; var mxEv = this.props.events[i];
@ -339,7 +339,7 @@ module.exports = React.createClass({
prevEvent = e; prevEvent = e;
return ret; return ret;
} }
).reduce((a,b) => a.concat(b)); ).reduce((a, b) => a.concat(b));
if (eventTiles.length === 0) { if (eventTiles.length === 0) {
eventTiles = null; eventTiles = null;

View File

@ -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,24 @@ module.exports = React.createClass({
}); });
}, },
// We don't need the actual height - just whether it is likely to have
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
_getSize: function(state, props) {
if (state.syncState === "ERROR" || state.whoisTypingString) {
return STATUS_BAR_EXPANDED;
} else if (props.tabCompleteEntries) {
return STATUS_BAR_HIDDEN;
} else if (props.hasUnsentMessages) {
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
},
// determine if we need to call onResize // determine if we need to call onResize
_checkForResize: function(prevProps, prevState) { _checkForResize: function(prevProps, prevState) {
// figure out the old height and the new height of the status bar. We // figure out the old height and the new height of the status bar.
// don't need the actual height - just whether it is likely to have return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state);
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
var oldSize, newSize;
if (prevState.syncState === "ERROR") {
oldSize = 1;
} else if (prevProps.tabCompleteEntries) {
oldSize = 0;
} else if (prevProps.hasUnsentMessages) {
oldSize = 2;
} else {
oldSize = 0;
}
if (this.state.syncState === "ERROR") {
newSize = 1;
} else if (this.props.tabCompleteEntries) {
newSize = 0;
} else if (this.props.hasUnsentMessages) {
newSize = 2;
} else {
newSize = 0;
}
return newSize != oldSize;
}, },
// return suitable content for the image on the left of the status bar. // return suitable content for the image on the left of the status bar.
@ -173,10 +189,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 +198,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() {

View File

@ -48,7 +48,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console); var debuglog = console.log.bind(console);
} else { } else {
var debuglog = function () {}; var debuglog = function() {};
} }
module.exports = React.createClass({ module.exports = React.createClass({
@ -146,7 +146,9 @@ module.exports = React.createClass({
showTopUnreadMessagesBar: false, showTopUnreadMessagesBar: false,
auxPanelMaxHeight: undefined, auxPanelMaxHeight: undefined,
}
statusBarVisible: false,
};
}, },
componentWillMount: function() { componentWillMount: function() {
@ -674,8 +676,9 @@ module.exports = React.createClass({
}, },
onSearchResultsFillRequest: function(backwards) { onSearchResultsFillRequest: function(backwards) {
if (!backwards) if (!backwards) {
return q(false); return q(false);
}
if (this.state.searchResults.next_batch) { if (this.state.searchResults.next_batch) {
debuglog("requesting more search results"); debuglog("requesting more search results");
@ -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
} }
@ -1183,7 +1186,7 @@ module.exports = React.createClass({
this.setState({ searching: true }); this.setState({ searching: true });
}, },
onCancelSearchClick: function () { onCancelSearchClick: function() {
this.setState({ this.setState({
searching: false, searching: false,
searchResults: null, searchResults: null,
@ -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 }

View File

@ -34,7 +34,7 @@ if (DEBUG_SCROLL) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console); var debuglog = console.log.bind(console);
} else { } else {
var debuglog = function () {}; var debuglog = function() {};
} }
/* This component implements an intelligent scrolling list. /* This component implements an intelligent scrolling list.
@ -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;
} }

View File

@ -38,7 +38,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console); var debuglog = console.log.bind(console);
} else { } else {
var debuglog = function () {}; var debuglog = function() {};
} }
/* /*
@ -322,7 +322,7 @@ var TimelinePanel = React.createClass({
}); });
}, },
onMessageListScroll: function () { onMessageListScroll: function() {
if (this.props.onScroll) { if (this.props.onScroll) {
this.props.onScroll(); this.props.onScroll();
} }
@ -387,7 +387,7 @@ var TimelinePanel = React.createClass({
// if we're at the end of the live timeline, append the pending events // if we're at the end of the live timeline, append the pending events
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.room.getPendingEvents()); events.push(...this.props.timelineSet.room.getPendingEvents());
} }
var updatedState = {events: events}; var updatedState = {events: events};
@ -564,8 +564,9 @@ var TimelinePanel = React.createClass({
// first find where the current RM is // first find where the current RM is
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
if (events[i].getId() == this.state.readMarkerEventId) if (events[i].getId() == this.state.readMarkerEventId) {
break; break;
}
} }
if (i >= events.length) { if (i >= events.length) {
return; return;
@ -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();
@ -868,7 +869,7 @@ var TimelinePanel = React.createClass({
// if we're at the end of the live timeline, append the pending events // if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.getPendingEvents()); events.push(...this.props.timelineSet.getPendingEvents());
} }
return events; return events;
@ -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);

View File

@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
// }]; // }];
if (uploads.length == 0) { if (uploads.length == 0) {
return <div /> return <div />;
} }
var upload; var upload;
@ -68,7 +68,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
} }
} }
if (!upload) { if (!upload) {
return <div /> return <div />;
} }
var innerProgressStyle = { var innerProgressStyle = {
@ -76,7 +76,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
}; };
var uploadedSize = filesize(upload.loaded); var uploadedSize = filesize(upload.loaded);
var totalSize = filesize(upload.total); var totalSize = filesize(upload.total);
if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) { if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
uploadedSize = uploadedSize.replace(/ .*/, ''); uploadedSize = uploadedSize.replace(/ .*/, '');
} }

View File

@ -32,6 +32,53 @@ var AddThreepid = require('../../AddThreepid');
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',
@ -93,6 +140,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() {
@ -293,8 +346,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,
@ -342,60 +395,68 @@ 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() }
<input id="urlPreviewsDisabled" { SETTINGS_LABELS.map( this._renderSyncedSetting ) }
type="checkbox" { THEMES.map( this._renderThemeSelector ) }
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
</label>
</div>
</div> </div>
{ settingsLabels.forEach( setting => {
<div className="mx_UserSettings_toggle">
<input id={ setting.id }
type="checkbox"
defaultChecked={ syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
<label htmlFor={ setting.id }>
{ settings.label }
</label>
</div>
})}
</div> </div>
); );
}, },
_renderUrlPreviewSelector: function() {
return <div className="mx_UserSettings_toggle">
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
</label>
</div>;
},
_renderSyncedSetting: function(setting) {
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
<label htmlFor={ setting.id }>
{ setting.label }
</label>
</div>;
},
_renderThemeSelector: function(setting) {
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
<input id={ setting.id + "_" + setting.value }
type="radio"
name={ setting.id }
value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
onChange={ e => {
if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
dis.dispatch({
action: 'set_theme',
value: setting.value,
});
}
}
/>
<label htmlFor={ setting.id + "_" + setting.value }>
{ setting.label }
</label>
</div>;
},
_renderCryptoInfo: function() { _renderCryptoInfo: function() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const deviceId = client.deviceId; const deviceId = client.deviceId;
@ -406,8 +467,8 @@ module.exports = React.createClass({
<h3>Cryptography</h3> <h3>Cryptography</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection"> <div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul> <ul>
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li> <li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li> <li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -424,7 +485,7 @@ module.exports = React.createClass({
); );
}, },
_renderLabs: function () { _renderLabs: function() {
// default to enabled if undefined // default to enabled if undefined
if (this.props.enableLabs === false) return null; if (this.props.enableLabs === false) return null;
@ -460,7 +521,7 @@ module.exports = React.createClass({
{features} {features}
</div> </div>
</div> </div>
) );
}, },
_renderDeactivateAccount: function() { _renderDeactivateAccount: function() {
@ -544,10 +605,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>
); );
@ -569,7 +630,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>
@ -650,7 +711,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>

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ module.exports = React.createClass({
height: 40, height: 40,
resizeMethod: 'crop', resizeMethod: 'crop',
defaultToInitialLetter: true defaultToInitialLetter: true
} };
}, },
getInitialState: function() { getInitialState: function() {

View File

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

View File

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

View File

@ -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() {

View File

@ -27,6 +27,15 @@ var Modal = require('../../../Modal');
const TRUNCATE_QUERY_LIST = 40; const TRUNCATE_QUERY_LIST = 40;
/*
* Escapes a string so it can be used in a RegExp
* Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ]
* From http://stackoverflow.com/a/6969486
*/
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
module.exports = React.createClass({ module.exports = React.createClass({
displayName: "ChatInviteDialog", displayName: "ChatInviteDialog",
propTypes: { propTypes: {
@ -48,7 +57,7 @@ module.exports = React.createClass({
title: "Start a chat", title: "Start a chat",
description: "Who would you like to communicate with?", description: "Who would you like to communicate with?",
value: "", value: "",
placeholder: "User ID, Name or email", placeholder: "Email, name or matrix ID",
button: "Start Chat", button: "Start Chat",
focus: true focus: true
}; };
@ -71,15 +80,12 @@ module.exports = React.createClass({
}, },
onButtonClick: function() { onButtonClick: function() {
var inviteList = this.state.inviteList.slice(); let inviteList = this.state.inviteList.slice();
// Check the text input field to see if user has an unconverted address // Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local inviteList // If there is and it's valid add it to the local inviteList
var check = Invite.isValidAddress(this.refs.textinput.value); if (this.refs.textinput.value !== '') {
if (check === true || check === null) { inviteList = this._addInputToList();
inviteList.push(this.refs.textinput.value); if (inviteList === null) return;
} else if (this.refs.textinput.value.length > 0) {
this.setState({ error: true });
return;
} }
if (inviteList.length > 0) { if (inviteList.length > 0) {
@ -119,15 +125,15 @@ module.exports = React.createClass({
} else if (e.keyCode === 38) { // up arrow } else if (e.keyCode === 38) { // up arrow
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.addressSelector.onKeyUp(); this.addressSelector.moveSelectionUp();
} else if (e.keyCode === 40) { // down arrow } else if (e.keyCode === 40) { // down arrow
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.addressSelector.onKeyDown(); this.addressSelector.moveSelectionDown();
} else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab } else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.addressSelector.onKeySelect(); this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace } else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -135,21 +141,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();
this.onButtonClick(); if (this.refs.textinput.value == '') {
// if there's nothing in the input box, submit the form
this.onButtonClick();
} else {
this._addInputToList();
}
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab } else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
var check = Invite.isValidAddress(this.refs.textinput.value); this._addInputToList();
if (check === true || check === null) {
var inviteList = this.state.inviteList.slice();
inviteList.push(this.refs.textinput.value.trim());
this.setState({
inviteList: inviteList,
queryList: [],
});
} else {
this.setState({ error: true });
}
} }
}, },
@ -179,7 +180,7 @@ module.exports = React.createClass({
inviteList: inviteList, inviteList: inviteList,
queryList: [], queryList: [],
}); });
} };
}, },
onClick: function(index) { onClick: function(index) {
@ -315,13 +316,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:
return true; // * The start of the string
} // * Whitespace, or
// * A fixed number of punctuation characters
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
if (expr.test(name)) {
return true;
} }
return false; return false;
}, },
@ -361,6 +367,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");
@ -394,13 +416,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 }
/>
); );
} }

View File

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

View File

@ -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() { selected: index,
self.setState({ hover: true,
selected: index, });
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>
); );

View File

@ -108,7 +108,7 @@ module.exports = React.createClass({
info = ( info = (
<div className={unknownMxClasses}>{ this.props.address }</div> <div className={unknownMxClasses}>{ this.props.address }</div>
); );
} else if (email) { } else if (email) {
var emailClasses = classNames({ var emailClasses = classNames({
"mx_AddressTile_email": true, "mx_AddressTile_email": true,
"mx_AddressTile_justified": this.props.justified, "mx_AddressTile_justified": this.props.justified,

View File

@ -42,8 +42,8 @@ export default React.createClass({
<div className="mx_UserSettings_cryptoSection"> <div className="mx_UserSettings_cryptoSection">
<ul> <ul>
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li> <li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li> <li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li> <li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
</ul> </ul>
</div> </div>
<p> <p>

View File

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

View File

@ -73,7 +73,7 @@ module.exports = React.createClass({
getValue: function() { getValue: function() {
var value; var value;
if (this.refs.select) { if (this.refs.select) {
value = reverseRoles[ this.refs.select.value ]; value = reverseRoles[this.refs.select.value];
if (this.refs.custom) { if (this.refs.custom) {
if (value === undefined) value = parseInt( this.refs.custom.value ); if (value === undefined) value = parseInt( this.refs.custom.value );
} }
@ -86,10 +86,10 @@ module.exports = React.createClass({
if (this.state.custom) { if (this.state.custom) {
var input; var input;
if (this.props.disabled) { if (this.props.disabled) {
input = <span>{ this.props.value }</span> input = <span>{ this.props.value }</span>;
} }
else { else {
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/> input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>;
} }
customPicker = <span> of { input }</span>; customPicker = <span> of { input }</span>;
} }
@ -115,7 +115,7 @@ module.exports = React.createClass({
<option value="Moderator">Moderator (50)</option> <option value="Moderator">Moderator (50)</option>
<option value="Admin">Admin (100)</option> <option value="Admin">Admin (100)</option>
<option value="Custom">Custom level</option> <option value="Custom">Custom level</option>
</select> </select>;
} }
return ( return (

View File

@ -35,4 +35,4 @@ module.exports = React.createClass({
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div> <div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
); );
} }
}); });

View File

@ -55,7 +55,7 @@ module.exports = React.createClass({
overflowJsx = this.props.createOverflowElement( overflowJsx = this.props.createOverflowElement(
overflowCount, childCount overflowCount, childCount
); );
// cut out the overflow elements // cut out the overflow elements
childArray.splice(childCount - overflowCount, overflowCount); childArray.splice(childCount - overflowCount, overflowCount);
childsJsx = childArray; // use what is left childsJsx = childArray; // use what is left

View File

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

View File

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

View File

@ -70,7 +70,7 @@ export const PasswordAuthEntry = React.createClass({
}); });
}, },
_onPasswordFieldChange: function (ev) { _onPasswordFieldChange: function(ev) {
// enable the submit button iff the password is non-empty // enable the submit button iff the password is non-empty
this.props.setSubmitButtonEnabled(Boolean(ev.target.value)); this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
}, },
@ -209,4 +209,4 @@ export function getEntryComponentForLoginType(loginType) {
} }
} }
return FallbackAuthEntry; return FallbackAuthEntry;
}; }

View File

@ -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 = (
<input type="text" ref="email" <div>
autoFocus={true} placeholder="Email address (optional)" <input type="text" ref="email"
defaultValue={this.props.defaultEmail} autoFocus={true} placeholder="Email address (optional)"
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} defaultValue={this.props.defaultEmail}
onBlur={function() {self.validateField(FIELD_EMAIL)}} /> className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
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&nbsp;
<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}

View File

@ -64,10 +64,10 @@ module.exports = React.createClass({
hs_url: this.props.customHsUrl, hs_url: this.props.customHsUrl,
is_url: this.props.customIsUrl, is_url: this.props.customIsUrl,
// if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible // if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible
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) {

View File

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

View File

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

View File

@ -119,7 +119,7 @@ module.exports = React.createClass({
if (content.info.thumbnail_file) { if (content.info.thumbnail_file) {
thumbnailPromise = decryptFile( thumbnailPromise = decryptFile(
content.info.thumbnail_file content.info.thumbnail_file
).then(function (blob) { ).then(function(blob) {
return readBlobAsDataUri(blob); return readBlobAsDataUri(blob);
}); });
} }

View File

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

View File

@ -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) {
@ -230,8 +230,8 @@ module.exports = React.createClass({
if (!confirmed) { if (!confirmed) {
return; return;
} }
let width = window.screen.width > 1024 ? 1024 : window.screen.width; let width = window.screen.width > 1024 ? 1024 : window.screen.width;
let height = window.screen.height > 800 ? 800 : window.screen.height; let height = window.screen.height > 800 ? 800 : window.screen.height;
let left = (window.screen.width - width) / 2; let left = (window.screen.width - width) / 2;
let top = (window.screen.height - height) / 2; let top = (window.screen.height - height) / 2;
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`); window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);

View File

@ -103,13 +103,13 @@ module.exports = React.createClass({
} }
if (oldCanonicalAlias !== this.state.canonicalAlias) { if (oldCanonicalAlias !== this.state.canonicalAlias) {
console.log("AliasSettings: Updating canonical alias"); console.log("AliasSettings: Updating canonical alias");
promises = [ q.all(promises).then( promises = [q.all(promises).then(
MatrixClientPeg.get().sendStateEvent( MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.canonical_alias", { this.props.roomId, "m.room.canonical_alias", {
alias: this.state.canonicalAlias alias: this.state.canonicalAlias
}, "" }, ""
) )
) ]; )];
} }
return promises; return promises;
@ -144,7 +144,7 @@ module.exports = React.createClass({
// XXX: do we need to deep copy aliases before editing it? // XXX: do we need to deep copy aliases before editing it?
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || []; this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
this.state.domainToAliases[domain].push(alias); this.state.domainToAliases[domain].push(alias);
this.setState({ this.setState({
domainToAliases: this.state.domainToAliases domainToAliases: this.state.domainToAliases
}); });
@ -152,9 +152,9 @@ module.exports = React.createClass({
this.refs.add_alias.setValue(''); // FIXME this.refs.add_alias.setValue(''); // FIXME
} }
else { else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Invalid alias format", title: "Invalid alias format",
description: "'" + alias + "' is not a valid format for an alias", description: "'" + alias + "' is not a valid format for an alias",
}); });
} }
@ -168,9 +168,9 @@ module.exports = React.createClass({
this.state.domainToAliases[domain][index] = alias; this.state.domainToAliases[domain][index] = alias;
} }
else { else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Invalid address format", title: "Invalid address format",
description: "'" + alias + "' is not a valid format for an address", description: "'" + alias + "' is not a valid format for an address",
}); });
} }
@ -183,7 +183,7 @@ module.exports = React.createClass({
// would be to arbitrarily deepcopy to a temp variable and then setState // would be to arbitrarily deepcopy to a temp variable and then setState
// that, but why bother when we can cut this corner. // that, but why bother when we can cut this corner.
var alias = this.state.domainToAliases[domain].splice(index, 1); var alias = this.state.domainToAliases[domain].splice(index, 1);
this.setState({ this.setState({
domainToAliases: this.state.domainToAliases domainToAliases: this.state.domainToAliases
}); });
}, },
@ -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>
@ -310,4 +310,4 @@ module.exports = React.createClass({
</div> </div>
); );
} }
}); });

View File

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

View File

@ -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 (
@ -154,4 +154,4 @@ module.exports = React.createClass({
); );
} }
}); });

View File

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

View File

@ -149,13 +149,13 @@ module.exports = WithMatrixClient(React.createClass({
this.props.mxEvent.on("Event.decrypted", this._onDecrypted); this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
}, },
componentWillReceiveProps: function (nextProps) { componentWillReceiveProps: function(nextProps) {
if (nextProps.mxEvent !== this.props.mxEvent) { if (nextProps.mxEvent !== this.props.mxEvent) {
this._verifyEvent(nextProps.mxEvent); this._verifyEvent(nextProps.mxEvent);
} }
}, },
shouldComponentUpdate: function (nextProps, nextState) { shouldComponentUpdate: function(nextProps, nextState) {
if (!ObjectUtils.shallowEqual(this.state, nextState)) { if (!ObjectUtils.shallowEqual(this.state, nextState)) {
return true; return true;
} }
@ -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;

View File

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

View File

@ -60,7 +60,7 @@ export default class MemberDeviceInfo extends React.Component {
</div> </div>
); );
} }
}; }
MemberDeviceInfo.displayName = 'MemberDeviceInfo'; MemberDeviceInfo.displayName = 'MemberDeviceInfo';
MemberDeviceInfo.propTypes = { MemberDeviceInfo.propTypes = {

View File

@ -64,7 +64,7 @@ module.exports = WithMatrixClient(React.createClass({
updating: 0, updating: 0,
devicesLoading: true, devicesLoading: true,
devices: null, devices: null,
} };
}, },
componentWillMount: function() { componentWillMount: function() {
@ -202,7 +202,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;
@ -376,6 +376,7 @@ module.exports = WithMatrixClient(React.createClass({
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Power change success"); console.log("Power change success");
}, function(err) { }, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failure to change power level", title: "Failure to change power level",
description: err.message description: err.message
@ -383,7 +384,7 @@ module.exports = WithMatrixClient(React.createClass({
} }
).finally(()=>{ ).finally(()=>{
this.setState({ updating: this.state.updating - 1 }); this.setState({ updating: this.state.updating - 1 });
}); }).done();
this.props.onFinished(); this.props.onFinished();
}, },
@ -528,7 +529,7 @@ module.exports = WithMatrixClient(React.createClass({
}); });
}, },
onMemberAvatarClick: function () { onMemberAvatarClick: function() {
var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url; var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url;
if(!avatarUrl) return; if(!avatarUrl) return;
@ -619,7 +620,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>
</div> </div>;
startChat = <div> startChat = <div>
<h3>Direct chats</h3> <h3>Direct chats</h3>
@ -653,7 +654,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 = <div className="mx_MemberInfo_field" onClick={this.onModToggle}> giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel} {giveOpLabel}
</div> </div>;
} }
// 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
@ -671,7 +672,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;

View File

@ -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',
@ -207,7 +207,7 @@ module.exports = React.createClass({
// For now we'll pretend this is any entity. It should probably be a separate tile. // For now we'll pretend this is any entity. It should probably be a separate tile.
var EntityTile = sdk.getComponent("rooms.EntityTile"); var EntityTile = sdk.getComponent("rooms.EntityTile");
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
return ( return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={ <EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} /> <BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
@ -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} />
) );
}) });
} }
} }

View File

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

View File

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

View File

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

View File

@ -192,7 +192,7 @@ module.exports = React.createClass({
} }
}, },
onKeyDown: function (ev) { onKeyDown: function(ev) {
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) { if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
var input = this.refs.textarea.value; var input = this.refs.textarea.value;
if (input.length === 0) { if (input.length === 0) {
@ -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);

View File

@ -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() {

View File

@ -182,8 +182,8 @@ module.exports = React.createClass({
'm.room.name', user_id 'm.room.name', user_id
); );
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</div> save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</div>;
cancel_button = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div> cancel_button = <div className="mx_RoomHeader_cancelButton mx_filterFlipColor" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>;
} }
if (this.props.saving) { if (this.props.saving) {
@ -193,7 +193,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;
@ -232,7 +232,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) {
@ -301,7 +301,7 @@ module.exports = React.createClass({
rightPanel_buttons = rightPanel_buttons =
<div className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<"> <div 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"/>
</div> </div>;
} }
var right_row; var right_row;

View File

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

View File

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

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