Merge remote-tracking branch 'origin/develop' into rav/hotkey-ux

This commit is contained in:
Richard van der Hoff 2017-01-24 20:47:24 +00:00
commit 6dd46d532a
124 changed files with 1837 additions and 654 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

@ -4,3 +4,6 @@ node_js:
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)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.3...v0.8.4)

View file

@ -19,7 +19,7 @@ npm install
npm run test
# 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
rm -f matrix-react-sdk-*.tgz

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.8.4",
"version": "0.8.5",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -10,6 +10,7 @@
"license": "Apache-2.0",
"main": "lib/index.js",
"files": [
".eslintrc.js",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
@ -58,7 +59,7 @@
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
"marked": "^0.3.5",
"commonmark": "^0.27.0",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1",
"q": "^1.4.1",
@ -67,13 +68,14 @@
"react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.11.1",
"text-encoding-utf-8": "^1.0.1",
"velocity-vector": "vector-im/velocity#059e3b2",
"whatwg-fetch": "^1.0.0"
},
"devDependencies": {
"babel-cli": "^6.5.2",
"babel-core": "^6.14.0",
"babel-eslint": "^6.1.0",
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-async-to-generator": "^6.16.0",
@ -85,9 +87,10 @@
"babel-preset-es2016": "^6.11.3",
"babel-preset-es2017": "^6.14.0",
"babel-preset-react": "^6.11.1",
"eslint": "^2.13.1",
"eslint-plugin-flowtype": "^2.17.0",
"eslint-plugin-react": "^6.2.1",
"eslint": "^3.13.1",
"eslint-config-google": "^0.7.1",
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^6.9.0",
"expect": "^1.16.0",
"json-loader": "^0.5.3",
"karma": "^0.13.22",

View file

@ -21,7 +21,7 @@ var MatrixClientPeg = require("./MatrixClientPeg");
* optionally, the identity servers.
*
* 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.
*/
class AddThreepid {

View file

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

View file

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

View file

@ -159,10 +159,10 @@ function _setCallState(call, roomId, status) {
calls[roomId] = call;
if (status === "ringing") {
play("ringAudio")
play("ringAudio");
}
else if (call && call.call_state === "ringing") {
pause("ringAudio")
pause("ringAudio");
}
if (call) {

View file

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

View file

@ -48,5 +48,5 @@ module.exports = {
//return pad(date.getHours()) + ':' + pad(date.getMinutes());
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2);
}
}
};

View file

@ -136,6 +136,6 @@ module.exports = {
fromUsers: function(users, showInviteButton, inviteFn) {
return users.map(function(u) {
return new UserEntity(u, showInviteButton, inviteFn);
})
});
}
};

View file

@ -91,16 +91,16 @@ var sanitizeHtmlParams = {
],
allowedAttributes: {
// custom ones first:
font: [ 'color' ], // custom to matrix
a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix
font: ['color'], // custom to matrix
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
// We don't currently allow img itself by default, but this
// 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
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
// DO NOT USE. sanitize-html allows all URL starting with '//'
// so this will always allow links to whatever scheme the

View file

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

View file

@ -55,29 +55,7 @@ export function inviteToRoom(roomId, addr) {
* @returns Promise
*/
export function inviteMultipleToRoom(roomId, addrs) {
this.inviter = new MultiInviter(roomId);
return this.inviter.invite(addrs);
const inviter = new MultiInviter(roomId);
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 MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier'
import Notifier from './Notifier';
import UserActivity from './UserActivity';
import Presence from './Presence';
import dis from './dispatcher';
@ -140,7 +140,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
homeserverUrl: queryParams.homeserver,
identityServerUrl: queryParams.identityServer,
guest: false,
})
});
}, (err) => {
console.error("Failed to log in with login token: " + err + " " +
err.data);

View file

@ -14,20 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import marked from 'marked';
// 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>)
});
import commonmark from 'commonmark';
/**
* 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 {
constructor(input) {
const lexer = new marked.Lexer(marked_options);
this.tokens = lexer.lex(input);
}
_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);
this.input = input;
this.parser = new commonmark.Parser();
this.renderer = new commonmark.HtmlRenderer({safe: false});
}
isPlainText() {
@ -64,65 +44,81 @@ export default class Markdown {
is_plain = false;
}
const dummy_renderer = {};
for (const k of Object.keys(marked.Renderer.prototype)) {
const dummy_renderer = new commonmark.HtmlRenderer();
for (const k of Object.keys(commonmark.HtmlRenderer.prototype)) {
dummy_renderer[k] = setNotPlain;
}
// text and paragraph are just text
dummy_renderer.text = function(t){return t;}
dummy_renderer.paragraph = function(t){return t;}
dummy_renderer.text = 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:
// this ignores plain URLs that markdown has
// 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());
const dummy_parser = new commonmark.Parser();
dummy_renderer.render(dummy_parser.parse(this.input));
return is_plain;
}
toHTML() {
const real_renderer = new marked.Renderer();
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);
}
const real_paragraph = this.renderer.paragraph;
real_renderer.paragraph = (text) => {
// The tokens at the top level are the 'blocks', so if we
// have more than one, there are multiple 'paragraphs'.
// If there is only one top level token, just return the
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 necessarily wrapped in its own
// p tag. If, however, we have multiple tokens, each gets
// '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.
if (this.tokens.length == 1) {
return text;
var par = node;
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, {
renderer: real_renderer,
});
const real_parser = new marked.Parser(real_options);
return real_parser.parse(this._copyTokens());
var parsed = this.parser.parse(this.input);
var rendered = this.renderer.render(parsed);
this.renderer.paragraph = real_paragraph;
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 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 = {
DialogContainerId: "mx_Dialog_Container",
@ -35,21 +84,47 @@ module.exports = {
return container;
},
createDialog: function (Element, props, className) {
var self = this;
createDialog: function(Element, props, className) {
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
},
/**
* Open a modal view.
*
* This can be used to display a react component which is loaded as an asynchronous
* webpack component. To do this, set 'loader' as:
*
* (cb) => {
* require(['<module>'], cb);
* }
*
* @param {Function} loader a function which takes a 'callback' argument,
* which it should call with a React component which will be displayed as
* the modal view.
*
* @param {Object} props properties to pass to the displayed
* component. (We will also pass an 'onFinished' property.)
*
* @param {String} className CSS class to apply to the modal wrapper
*/
createDialogAsync: function(loader, props, className) {
var self = this;
// never call this via modal.close() from onFinished() otherwise it will loop
var closeDialog = function() {
if (props && props.onFinished) props.onFinished.apply(null, arguments);
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
// property set here so you can't close the dialog from a button click!
var dialog = (
<div className={"mx_Dialog_wrapper " + className}>
<div className="mx_Dialog">
<Element {...props} onFinished={closeDialog}/>
<AsyncWrapper key={modalCount} loader={loader} {...props} onFinished={closeDialog}/>
</div>
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -37,7 +37,7 @@ export function getOnlyOtherMember(room, me) {
if (joinedMembers.length === 2) {
return joinedMembers.filter(function(m) {
return m.userId !== me.userId
return m.userId !== me.userId;
})[0];
}

View file

@ -371,7 +371,7 @@ const onMessage = function(event) {
}, (err) => {
console.error(err);
sendError(event, "Failed to lookup current room.");
})
});
};
module.exports = {

View file

@ -203,7 +203,17 @@ class Register extends Signup {
} else if (error.errcode == 'M_INVALID_USERNAME') {
throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
} 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) {
throw new Error(
`Server error during registration! (${error.httpStatus})`

View file

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

View file

@ -112,7 +112,7 @@ class TabComplete {
return;
}
// ES6 destructuring; ignore first element (the complete match)
var [ , boundaryGroup, partialGroup] = res;
var [, boundaryGroup, partialGroup] = res;
if (partialGroup.length === 0 && passive) {
return;
@ -254,7 +254,7 @@ class TabComplete {
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
// 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)
ev.preventDefault();
@ -386,6 +386,6 @@ class TabComplete {
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
}
}
};
}
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
limitations under the License.
*/
var React = require("react");
var sdk = require("./index");
class Entry {
@ -90,7 +89,7 @@ CommandEntry.fromCommands = function(commandArray) {
return commandArray.map(function(cmd) {
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
});
}
};
class MemberEntry extends Entry {
constructor(member) {
@ -119,7 +118,7 @@ MemberEntry.fromMemberList = function(members) {
return members.map(function(m) {
return new MemberEntry(m);
});
}
};
module.exports.Entry = Entry;
module.exports.MemberEntry = MemberEntry;

View file

@ -75,7 +75,6 @@ function textForMemberEvent(ev) {
return targetName + " joined the room.";
}
}
return '';
case 'leave':
if (ev.getSender() === ev.getStateKey()) {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
@ -203,4 +202,4 @@ module.exports = {
if (!hdlr) return "";
return hdlr(ev);
}
}
};

View file

@ -14,9 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var dis = require("./dispatcher");
var sdk = require("./index");
// FIXME: these vars should be bundled up and attached to
// module.exports otherwise this will break when included by both
// react-sdk and apps layered on top.
@ -42,6 +39,7 @@ var keyHex = [
"#76CFA6", // Vector Green
"#EAF5F0", // 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
@ -50,6 +48,7 @@ var colors = [
keyHex[0],
keyHex[1],
keyHex[2],
keyHex[3],
];
var cssFixups = [
@ -150,7 +149,7 @@ function hexToRgb(color) {
function rgbToHex(rgb) {
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.
@ -185,7 +184,7 @@ module.exports = {
}
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);
rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255;
@ -194,7 +193,7 @@ module.exports = {
}
if (!tertiaryColor) {
var x = 0.19;
const x = 0.19;
var rgb1 = hexToRgb(primaryColor);
var rgb2 = hexToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
@ -210,7 +209,9 @@ module.exports = {
return;
}
colors = [primaryColor, secondaryColor, tertiaryColor];
colors[0] = primaryColor;
colors[1] = secondaryColor;
colors[2] = tertiaryColor;
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
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now.

View file

@ -76,7 +76,7 @@ module.exports = React.createClass({
var startStyles = self.props.startStyles;
if (startStyles.length > 0) {
var startStyle = startStyles[0]
var startStyle = startStyles[0];
newProps.style = startStyle;
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
}
@ -105,7 +105,7 @@ module.exports = React.createClass({
) {
var startStyles = this.props.startStyles;
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
// to start with, so now we animate 1 etc.
for (var i = 1; i < startStyles.length; ++i) {
@ -145,7 +145,7 @@ module.exports = React.createClass({
// and the FAQ entry, "Preventing memory leaks when
// creating/destroying large numbers of elements"
// (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);
}
this.nodes[k] = node;

View file

@ -6,10 +6,12 @@ function bounce( p ) {
var pow2,
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 );
}
Velocity.Easings.easeOutBounce = function(p) {
return 1 - bounce(1 - p);
}
};

View file

@ -46,4 +46,4 @@ module.exports = {
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;
let match;
while ((match = commandRegex.exec(query)) != null) {
let matchStart = match.index,

View file

@ -83,7 +83,7 @@ export default class CommandProvider extends AutocompleteProvider {
static getInstance(): CommandProvider {
if (instance == null)
instance = new CommandProvider();
{instance = new CommandProvider();}
return instance;
}

View file

@ -13,7 +13,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
constructor() {
super(DDG_REGEX);
}
static getQueryUri(query: String) {
return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
+ `&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() {
if (instance == null)
instance = new EmojiProvider();
{instance = new EmojiProvider();}
return instance;
}

View file

@ -52,12 +52,12 @@ export default class RoomProvider extends AutocompleteProvider {
getName() {
return '💬 Rooms';
}
static getInstance() {
if (instance == null) {
instance = new RoomProvider();
}
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);
import views$dialogs$DeactivateAccountDialog from './components/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';
views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';

View file

@ -47,7 +47,7 @@ module.exports = {
return container;
},
createMenu: function (Element, props) {
createMenu: function(Element, props) {
var self = this;
var closeMenu = function() {
@ -67,7 +67,7 @@ module.exports = {
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 = "";
if (props.menuColour) {
chevronCSS = `
@ -78,15 +78,15 @@ module.exports = {
.mx_ContextualMenu_chevron_right:after {
border-left-color: ${props.menuColour};
}
`
`;
}
var chevron = null;
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;
} 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;
}

View file

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

View file

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

View file

@ -160,8 +160,8 @@ export default React.createClass({
collapsedRhs={this.props.collapse_rhs}
ConferenceHandler={this.props.ConferenceHandler}
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;
case PageTypes.UserSettings:
@ -170,28 +170,28 @@ export default React.createClass({
brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs}
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;
case PageTypes.CreateRoom:
page_element = <CreateRoom
onRoomCreated={this.props.onRoomCreated}
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;
case PageTypes.RoomDirectory:
page_element = <RoomDirectory
collapsedRhs={this.props.collapse_rhs}
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;
case PageTypes.UserView:
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;
}

View file

@ -77,7 +77,7 @@ module.exports = React.createClass({
getChildContext: function() {
return {
appConfig: this.props.config,
}
};
},
getInitialState: function() {
@ -456,6 +456,9 @@ module.exports = React.createClass({
middleOpacity: payload.middleOpacity,
});
break;
case 'set_theme':
this._onSetTheme(payload.value);
break;
case 'on_logged_in':
this._onLoggedIn();
break;
@ -586,6 +589,50 @@ module.exports = React.createClass({
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
*/
@ -687,6 +734,16 @@ module.exports = React.createClass({
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) {
@ -979,7 +1036,7 @@ module.exports = React.createClass({
{...this.props}
{...this.state}
/>
)
);
} else if (this.state.logged_in) {
// we think we are logged in, but are still waiting for the /sync to complete
var Spinner = sdk.getComponent('elements.Spinner');
@ -1003,6 +1060,7 @@ module.exports = React.createClass({
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
teamsConfig={this.props.config.teamsConfig}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl}

View file

@ -19,7 +19,7 @@ var ReactDOM = require("react-dom");
var dis = require("../../dispatcher");
var sdk = require('../../index');
var MatrixClientPeg = require('../../MatrixClientPeg')
var MatrixClientPeg = require('../../MatrixClientPeg');
const MILLIS_IN_DAY = 86400000;
@ -282,7 +282,7 @@ module.exports = React.createClass({
var isMembershipChange = (e) =>
e.getType() === 'm.room.member'
&& ['join', 'leave'].indexOf(e.getContent().membership) !== -1
&& (!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++) {
var mxEv = this.props.events[i];
@ -340,7 +340,7 @@ module.exports = React.createClass({
prevEvent = e;
return ret;
}
).reduce((a,b) => a.concat(b));
).reduce((a, b) => a.concat(b));
if (eventTiles.length === 0) {
eventTiles = null;

View file

@ -19,6 +19,14 @@ var sdk = require('../../index');
var dis = require("../../dispatcher");
var WhoIsTyping = require("../../WhoIsTyping");
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({
displayName: 'RoomStatusBar',
@ -60,6 +68,13 @@ module.exports = React.createClass({
// status bar. This is used to trigger a re-layout in the parent
// component.
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() {
@ -78,6 +93,18 @@ module.exports = React.createClass({
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
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() {
@ -104,35 +131,28 @@ 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 ||
props.numUnreadMessages ||
!props.atEndOfLiveTimeline ||
props.hasActiveCall) {
return STATUS_BAR_EXPANDED;
} else if (props.tabCompleteEntries) {
return STATUS_BAR_HIDDEN;
} else if (props.hasUnsentMessages) {
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
},
// determine if we need to call onResize
_checkForResize: function(prevProps, prevState) {
// figure out the old height and the new height of the status bar. We
// don't need the actual height - just whether it is likely to have
// changed - so we use '0' to indicate normal size, and other values to
// 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;
// figure out the old height and the new height of the status bar.
return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state);
},
// return suitable content for the image on the left of the status bar.
@ -173,10 +193,8 @@ module.exports = React.createClass({
if (wantPlaceholder) {
return (
<div className="mx_RoomStatusBar_placeholderIndicator">
<span>.</span>
<span>.</span>
<span>.</span>
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
{this._renderTypingIndicatorAvatars(TYPING_AVATARS_LIMIT)}
</div>
);
}
@ -184,6 +202,36 @@ module.exports = React.createClass({
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.
_getContent: function() {

View file

@ -48,7 +48,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
module.exports = React.createClass({
@ -146,7 +146,9 @@ module.exports = React.createClass({
showTopUnreadMessagesBar: false,
auxPanelMaxHeight: undefined,
}
statusBarVisible: false,
};
},
componentWillMount: function() {
@ -674,8 +676,9 @@ module.exports = React.createClass({
},
onSearchResultsFillRequest: function(backwards) {
if (!backwards)
if (!backwards) {
return q(false);
}
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
@ -758,7 +761,7 @@ module.exports = React.createClass({
}).then(() => {
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
{ inviteSignUrl: sign_url } )
{ inviteSignUrl: sign_url } );
}).then(function(resp) {
var roomId = resp.roomId;
@ -962,7 +965,7 @@ module.exports = React.createClass({
// For overlapping highlights,
// favour longer (more specific) terms first
highlights = highlights.sort(function(a, b) {
return b.length - a.length });
return b.length - a.length; });
self.setState({
searchHighlights: highlights,
@ -1025,7 +1028,7 @@ module.exports = React.createClass({
if (scrollPanel) {
scrollPanel.checkScroll();
}
}
};
var lastRoomId;
@ -1090,7 +1093,7 @@ module.exports = React.createClass({
}
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);
if (fails.length) {
fails.forEach(function(result) {
@ -1099,7 +1102,7 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
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
}
@ -1183,7 +1186,7 @@ module.exports = React.createClass({
this.setState({ searching: true });
},
onCancelSearchClick: function () {
onCancelSearchClick: function() {
this.setState({
searching: false,
searchResults: null,
@ -1208,8 +1211,9 @@ module.exports = React.createClass({
// decide whether or not the top 'unread messages' bar should be shown
_updateTopUnreadMessagesBar: function() {
if (!this.refs.messagePanel)
if (!this.refs.messagePanel) {
return;
}
var pos = this.refs.messagePanel.getReadMarkerPosition();
@ -1331,6 +1335,18 @@ module.exports = React.createClass({
// no longer anything to do here
},
onStatusBarVisible: function() {
this.setState({
statusBarVisible: true,
});
},
onStatusBarHidden: function() {
this.setState({
statusBarVisible: false,
});
},
showSettings: function(show) {
// XXX: this is a bit naughty; we should be doing this via props
if (show) {
@ -1498,7 +1514,7 @@ module.exports = React.createClass({
if (ContentMessages.getCurrentUploads().length > 0) {
var UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />
statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
@ -1513,7 +1529,9 @@ module.exports = React.createClass({
onCancelAllClick={this.onCancelAllClick}
onScrollToBottomClick={this.jumpToLiveTimeline}
onResize={this.onChildResize}
/>
onVisible={this.onStatusBarVisible}
onHidden={this.onStatusBarHidden}
/>;
}
var aux = null;
@ -1569,7 +1587,7 @@ module.exports = React.createClass({
messageComposer =
<MessageComposer
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
@ -1597,14 +1615,14 @@ module.exports = React.createClass({
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
width="31" height="27"/>
</div>
</div>;
}
voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
width="21" height="26"/>
</div>
</div>;
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
statusBar =
@ -1614,7 +1632,7 @@ module.exports = React.createClass({
{ zoomButton }
{ statusBar }
<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
@ -1667,6 +1685,10 @@ module.exports = React.createClass({
</div>
);
}
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
if (this.state.statusBarVisible) {
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
}
return (
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
@ -1689,7 +1711,7 @@ module.exports = React.createClass({
{ topUnreadMessagesBar }
{ messagePanel }
{ 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_line"></div>
{ statusBar }

View file

@ -34,7 +34,7 @@ if (DEBUG_SCROLL) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
/* This component implements an intelligent scrolling list.
@ -600,7 +600,7 @@ module.exports = React.createClass({
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollToken,
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}
};
debuglog("Saved scroll state", this.scrollState);
return;
}

View file

@ -38,7 +38,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
/*
@ -322,7 +322,7 @@ var TimelinePanel = React.createClass({
});
},
onMessageListScroll: function () {
onMessageListScroll: function() {
if (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 (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};
@ -564,8 +564,9 @@ var TimelinePanel = React.createClass({
// first find where the current RM is
for (var i = 0; i < events.length; i++) {
if (events[i].getId() == this.state.readMarkerEventId)
if (events[i].getId() == this.state.readMarkerEventId) {
break;
}
}
if (i >= events.length) {
return;
@ -644,7 +645,7 @@ var TimelinePanel = React.createClass({
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
var rmTs;
if (tl) {
var event = tl.getEvents().find((e) => { return e.getId() == rmId });
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
if (event) {
rmTs = event.getTs();
}
@ -821,7 +822,7 @@ var TimelinePanel = React.createClass({
description: message,
onFinished: onFinished,
});
}
};
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
@ -843,7 +844,7 @@ var TimelinePanel = React.createClass({
timelineLoading: true,
});
prom = prom.then(onLoaded, onError)
prom = prom.then(onLoaded, onError);
}
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 (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.getPendingEvents());
events.push(...this.props.timelineSet.getPendingEvents());
}
return events;
@ -930,8 +931,9 @@ var TimelinePanel = React.createClass({
_getCurrentReadReceipt: function(ignoreSynthesized) {
var client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null)
if (client == null) {
return null;
}
var myUserId = client.credentials.userId;
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);

View file

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

View file

@ -33,6 +33,53 @@ var AccessibleButton = require('../views/elements/AccessibleButton');
const REACT_SDK_VERSION =
'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({
displayName: 'UserSettings',
@ -94,6 +141,12 @@ module.exports = React.createClass({
middleOpacity: 0.3,
});
this._refreshFromServer();
var syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
},
componentDidMount: function() {
@ -294,8 +347,8 @@ module.exports = React.createClass({
this.setState({email_add_pending: false});
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
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."
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.";
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
description: message,
@ -343,60 +396,68 @@ module.exports = React.createClass({
_renderUserInterfaceSettings: function() {
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 (
<div>
<h3>User Interface</h3>
<div className="mx_UserSettings_section">
<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>
{ this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) }
</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>
);
},
_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() {
const client = MatrixClientPeg.get();
const deviceId = client.deviceId;
@ -407,8 +468,8 @@ module.exports = React.createClass({
<h3>Cryptography</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul>
<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 ID:</label> <span><code>{deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
</ul>
</div>
</div>
@ -425,7 +486,7 @@ module.exports = React.createClass({
);
},
_renderLabs: function () {
_renderLabs: function() {
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
@ -461,7 +522,7 @@ module.exports = React.createClass({
{features}
</div>
</div>
)
);
},
_renderDeactivateAccount: function() {
@ -545,10 +606,10 @@ module.exports = React.createClass({
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<input key={val.address} id={id} value={val.address} disabled />
<input type="text" key={val.address} id={id} value={val.address} disabled />
</div>
<div className="mx_UserSettings_threepidButton">
<img src="img/icon_context_delete.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
</div>
</div>
);
@ -570,7 +631,7 @@ module.exports = React.createClass({
blurToCancel={ false }
onValueChanged={ this.onAddThreepidClicked } />
</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) }/>
</div>
</div>
@ -651,7 +712,7 @@ module.exports = React.createClass({
</div>
<div className="mx_UserSettings_avatarPicker_edit">
<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"
width="17" height="15" />
</label>

View file

@ -58,7 +58,7 @@ module.exports = React.createClass({
this.setState({
progress: null
});
})
});
},
onVerify: function(ev) {
@ -71,7 +71,7 @@ module.exports = React.createClass({
this.setState({ progress: "complete" });
}, (err) => {
this.showErrorDialog(err.message);
})
});
},
onSubmitForm: function(ev) {
@ -129,7 +129,7 @@ module.exports = React.createClass({
var resetPasswordJsx;
if (this.state.progress === "sending_email") {
resetPasswordJsx = <Spinner />
resetPasswordJsx = <Spinner />;
}
else if (this.state.progress === "sent_email") {
resetPasswordJsx = (

View file

@ -173,7 +173,7 @@ module.exports = React.createClass({
},
_getCurrentFlowStep: function() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
},
_setStateFromError: function(err, isLoginAttempt) {
@ -195,7 +195,7 @@ module.exports = React.createClass({
}
let errorText = "Error: Problem communicating with the given homeserver " +
(errCode ? "(" + errCode + ")" : "")
(errCode ? "(" + errCode + ")" : "");
if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' &&
@ -258,7 +258,7 @@ module.exports = React.createClass({
loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
Login as guest
</a>
</a>;
}
var returnToAppJsx;
@ -266,7 +266,7 @@ module.exports = React.createClass({
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
</a>
</a>;
}
return (

View file

@ -49,6 +49,21 @@ module.exports = React.createClass({
email: React.PropTypes.string,
username: 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,
@ -169,6 +184,26 @@ module.exports = React.createClass({
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) {
MatrixClientPeg.get().getPushers().done((resp)=>{
var pushers = resp.pushers;
@ -254,6 +289,7 @@ module.exports = React.createClass({
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPassword={this.state.formVals.password}
teamsConfig={this.props.teamsConfig}
guestUsername={this.props.username}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
@ -297,7 +333,7 @@ module.exports = React.createClass({
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
</a>
</a>;
}
return (

View file

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

View file

@ -42,7 +42,7 @@ module.exports = React.createClass({
height: 40,
resizeMethod: 'crop',
viewUserOnClick: false,
}
};
},
getInitialState: function() {
@ -64,7 +64,7 @@ module.exports = React.createClass({
props.width,
props.height,
props.resizeMethod)
}
};
},
render: function() {
@ -78,7 +78,7 @@ module.exports = React.createClass({
action: 'view_user',
member: this.props.member,
});
}
};
}
return (

View file

@ -39,7 +39,7 @@ module.exports = React.createClass({
height: 36,
resizeMethod: 'crop',
oobData: {},
}
};
},
getInitialState: function() {
@ -51,7 +51,7 @@ module.exports = React.createClass({
componentWillReceiveProps: function(newProps) {
this.setState({
urls: this.getImageUrls(newProps)
})
});
},
getImageUrls: function(props) {

View file

@ -40,7 +40,7 @@ module.exports = React.createClass({
},
onValueChanged: function(ev) {
this.props.onChange(ev.target.value)
this.props.onChange(ev.target.value);
},
render: function() {

View file

@ -28,6 +28,15 @@ var AccessibleButton = require('../elements/AccessibleButton');
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({
displayName: "ChatInviteDialog",
propTypes: {
@ -72,15 +81,12 @@ module.exports = React.createClass({
},
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
// If there is and it's valid add it to the local inviteList
var check = Invite.isValidAddress(this.refs.textinput.value);
if (check === true || check === null) {
inviteList.push(this.refs.textinput.value);
} else if (this.refs.textinput.value.length > 0) {
this.setState({ error: true });
return;
if (this.refs.textinput.value !== '') {
inviteList = this._addInputToList();
if (inviteList === null) return;
}
if (inviteList.length > 0) {
@ -120,15 +126,15 @@ module.exports = React.createClass({
} else if (e.keyCode === 38) { // up arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeyUp();
this.addressSelector.moveSelectionUp();
} else if (e.keyCode === 40) { // down arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeyDown();
} else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
this.addressSelector.moveSelectionDown();
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeySelect();
this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
e.stopPropagation();
e.preventDefault();
@ -136,21 +142,16 @@ module.exports = React.createClass({
} else if (e.keyCode === 13) { // enter
e.stopPropagation();
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
e.stopPropagation();
e.preventDefault();
var check = Invite.isValidAddress(this.refs.textinput.value);
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 });
}
this._addInputToList();
}
},
@ -180,7 +181,7 @@ module.exports = React.createClass({
inviteList: inviteList,
queryList: [],
});
}
};
},
onClick: function(index) {
@ -316,13 +317,18 @@ module.exports = React.createClass({
return true;
}
// split spaces in name and try matching constituent parts
var parts = name.split(" ");
for (var i = 0; i < parts.length; i++) {
if (parts[i].indexOf(query) === 0) {
return true;
}
// Try to find the query following a "word boundary", except that
// this does avoids using \b because it only considers letters from
// the roman alphabet to be word characters.
// Instead, we look for the query following either:
// * The start of the string
// * Whitespace, or
// * A fixed number of punctuation characters
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
if (expr.test(name)) {
return true;
}
return false;
},
@ -362,6 +368,22 @@ module.exports = React.createClass({
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() {
var TintableSvg = sdk.getComponent("elements.TintableSvg");
var AddressSelector = sdk.getComponent("elements.AddressSelector");
@ -395,13 +417,18 @@ module.exports = React.createClass({
var error;
var addressSelector;
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 {
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
Searching known users
</div>;
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref}}
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
addressList={ this.state.queryList }
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;
if (this.state.errStr) {
error = <div className="error">
{this.state.err_str}
</div>
{this.state.errStr}
</div>;
passwordBoxClass = 'error';
}
@ -92,7 +92,7 @@ export default class DeactivateAccountDialog extends React.Component {
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
Cancel
</button>
</button>;
}
return (

View file

@ -28,6 +28,9 @@ module.exports = React.createClass({
addressList: React.PropTypes.array.isRequired,
truncateAt: React.PropTypes.number.isRequired,
selected: React.PropTypes.number,
// Element to put as a header on top of the list
header: React.PropTypes.node,
},
getInitialState: function() {
@ -55,7 +58,7 @@ module.exports = React.createClass({
}
},
onKeyUp: function() {
moveSelectionUp: function() {
if (this.state.selected > 0) {
this.setState({
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)) {
this.setState({
selected: this.state.selected + 1,
@ -73,25 +76,19 @@ module.exports = React.createClass({
}
},
onKeySelect: function() {
chooseSelection: function() {
this.selectAddress(this.state.selected);
},
onClick: function(index) {
var self = this;
return function() {
self.selectAddress(index);
};
this.selectAddress(index);
},
onMouseEnter: function(index) {
var self = this;
return function() {
self.setState({
selected: index,
hover: true,
});
};
this.setState({
selected: index,
hover: true,
});
},
onMouseLeave: function() {
@ -124,7 +121,7 @@ module.exports = React.createClass({
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
// method, how far to scroll when using the arrow keys
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" />
</div>
);
@ -135,7 +132,7 @@ module.exports = React.createClass({
_maxSelected: function(list) {
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;
},
@ -146,7 +143,8 @@ module.exports = React.createClass({
});
return (
<div className={classes} ref={(ref) => {this.scrollElement = ref}}>
<div className={classes} ref={(ref) => {this.scrollElement = ref;}}>
{ this.props.header }
{ this.createAddressListTiles() }
</div>
);

View file

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

View file

@ -42,8 +42,8 @@ export default React.createClass({
<div className="mx_UserSettings_cryptoSection">
<ul>
<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 key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></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>
</ul>
</div>
<p>

View file

@ -57,7 +57,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
phase: this.Phases.Display,
}
};
},
componentWillReceiveProps: function(nextProps) {
@ -164,7 +164,7 @@ module.exports = React.createClass({
this.setState({
phase: this.Phases.Edit,
})
});
},
onFocus: function(ev) {
@ -197,9 +197,9 @@ module.exports = React.createClass({
sel.removeAllRanges();
if (this.props.blurToCancel)
this.cancelEdit();
{this.cancelEdit();}
else
this.onFinish(ev);
{this.onFinish(ev);}
this.showPlaceholder(!this.value);
},

View file

@ -73,7 +73,7 @@ module.exports = React.createClass({
getValue: function() {
var value;
if (this.refs.select) {
value = reverseRoles[ this.refs.select.value ];
value = reverseRoles[this.refs.select.value];
if (this.refs.custom) {
if (value === undefined) value = parseInt( this.refs.custom.value );
}
@ -86,10 +86,10 @@ module.exports = React.createClass({
if (this.state.custom) {
var input;
if (this.props.disabled) {
input = <span>{ this.props.value }</span>
input = <span>{ this.props.value }</span>;
}
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>;
}
@ -115,7 +115,7 @@ module.exports = React.createClass({
<option value="Moderator">Moderator (50)</option>
<option value="Admin">Admin (100)</option>
<option value="Custom">Custom level</option>
</select>
</select>;
}
return (

View file

@ -35,4 +35,4 @@ module.exports = React.createClass({
<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(
overflowCount, childCount
);
// cut out the overflow elements
childArray.splice(childCount - overflowCount, overflowCount);
childsJsx = childArray; // use what is left

View file

@ -56,7 +56,7 @@ module.exports = React.createClass({
<div>
<ul className="mx_UserSelector_UserIdList" ref="list">
{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>
<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();
} else {
console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded()};
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
var protocol = global.location.protocol;
if (protocol === "file:") {
var warning = document.createElement('div');
@ -101,7 +101,7 @@ module.exports = React.createClass({
} catch (e) {
this.setState({
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
this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
},
@ -209,4 +209,4 @@ export function getEntryComponentForLoginType(loginType) {
}
}
return FallbackAuthEntry;
};
}

View file

@ -38,6 +38,16 @@ module.exports = React.createClass({
defaultEmail: React.PropTypes.string,
defaultUsername: 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.
// Specifying this param will also warn the user that entering
@ -62,7 +72,8 @@ module.exports = React.createClass({
getInitialState: function() {
return {
fieldValid: {}
fieldValid: {},
selectedTeam: null,
};
},
@ -105,10 +116,14 @@ module.exports = React.createClass({
},
_doSubmit: function() {
let email = this.refs.email.value.trim();
if (this.state.selectedTeam) {
email += "@" + this.state.selectedTeam.emailSuffix;
}
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim() || this.props.guestUsername,
password: this.refs.password.value.trim(),
email: this.refs.email.value.trim()
email: email,
});
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
* they were validated.
@ -135,15 +169,19 @@ module.exports = React.createClass({
validateField: function(field_id) {
var pwd1 = this.refs.password.value.trim();
var pwd2 = this.refs.passwordConfirm.value.trim()
var pwd2 = this.refs.passwordConfirm.value.trim();
switch (field_id) {
case FIELD_EMAIL:
this.markFieldValid(
field_id,
this.refs.email.value == '' || Email.looksValid(this.refs.email.value),
"RegistrationForm.ERR_EMAIL_INVALID"
);
let email = this.refs.email.value;
if (this.props.teamsConfig) {
let team = this.state.selectedTeam;
if (team) {
email = email + "@" + team.emailSuffix;
}
}
let valid = email === '' || Email.looksValid(email);
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
break;
case FIELD_USERNAME:
// XXX: SPEC-1
@ -222,17 +260,64 @@ module.exports = React.createClass({
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() {
var self = this;
var emailSection, registerButton;
var emailSection, teamSection, teamAdditionSupport, registerButton;
if (this.props.showEmail) {
let emailSuffix = this._renderEmailInputSuffix();
emailSection = (
<input type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
<div>
<input type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
defaultValue={this.props.defaultEmail}
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) {
registerButton = (
@ -242,31 +327,34 @@ module.exports = React.createClass({
var placeholderUserName = "User name";
if (this.props.guestUsername) {
placeholderUserName += " (default: " + this.props.guestUsername + ")"
placeholderUserName += " (default: " + this.props.guestUsername + ")";
}
return (
<div>
<form onSubmit={this.onSubmit}>
{teamSection}
{teamAdditionSupport}
<br />
{emailSection}
<br />
<input type="text" ref="username"
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_USERNAME)}} />
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
<br />
{ this.props.guestUsername ?
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
}
<input type="password" ref="password"
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} />
<br />
<input type="password" ref="passwordConfirm"
placeholder="Confirm password"
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} />
<br />
{registerButton}

View file

@ -64,10 +64,10 @@ module.exports = React.createClass({
hs_url: this.props.customHsUrl,
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
configVisible: !this.props.withToggleButton ||
configVisible: !this.props.withToggleButton ||
(this.props.customHsUrl !== this.props.defaultHsUrl) ||
(this.props.customIsUrl !== this.props.defaultIsUrl)
}
};
},
onHomeserverChanged: function(ev) {

View file

@ -31,7 +31,7 @@ export default class MAudioBody extends React.Component {
decryptedUrl: null,
decryptedBlob: null,
error: null,
}
};
}
onPlayToggle() {
this.setState({

View file

@ -281,7 +281,7 @@ module.exports = React.createClass({
decryptedBlob: blob,
});
}).catch((err) => {
console.warn("Unable to decrypt attachment: ", err)
console.warn("Unable to decrypt attachment: ", err);
Modal.createDialog(ErrorDialog, {
description: "Error decrypting attachment"
});
@ -372,7 +372,7 @@ module.exports = React.createClass({
var extra = text ? (': ' + text) : '';
return <span className="mx_MFileBody">
Invalid file{extra}
</span>
</span>;
}
},
});

View file

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

View file

@ -111,7 +111,7 @@ module.exports = React.createClass({
this.props.onWidgetLoad();
});
}).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.
this.setState({
error: err,

View file

@ -200,7 +200,7 @@ module.exports = React.createClass({
global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId());
}
},
}
};
},
onStarterLinkClick: function(starterLink, ev) {
@ -230,8 +230,8 @@ module.exports = React.createClass({
if (!confirmed) {
return;
}
let width = window.screen.width > 1024 ? 1024 : window.screen.width;
let height = window.screen.height > 800 ? 800 : window.screen.height;
let width = window.screen.width > 1024 ? 1024 : window.screen.width;
let height = window.screen.height > 800 ? 800 : window.screen.height;
let left = (window.screen.width - width) / 2;
let top = (window.screen.height - height) / 2;
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) {
console.log("AliasSettings: Updating canonical alias");
promises = [ q.all(promises).then(
promises = [q.all(promises).then(
MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.canonical_alias", {
alias: this.state.canonicalAlias
}, ""
)
) ];
)];
}
return promises;
@ -144,7 +144,7 @@ module.exports = React.createClass({
// XXX: do we need to deep copy aliases before editing it?
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
this.state.domainToAliases[domain].push(alias);
this.setState({
this.setState({
domainToAliases: this.state.domainToAliases
});
@ -152,9 +152,9 @@ module.exports = React.createClass({
this.refs.add_alias.setValue(''); // FIXME
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Invalid alias format",
title: "Invalid alias format",
description: "'" + alias + "' is not a valid format for an alias",
});
}
@ -168,9 +168,9 @@ module.exports = React.createClass({
this.state.domainToAliases[domain][index] = alias;
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Invalid address format",
title: "Invalid address format",
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
// that, but why bother when we can cut this corner.
var alias = this.state.domainToAliases[domain].splice(index, 1);
this.setState({
this.setState({
domainToAliases: this.state.domainToAliases
});
},
@ -281,7 +281,7 @@ module.exports = React.createClass({
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
editable={ self.props.canSetAliases }
initialValue={ alias } />
<div className="mx_RoomSettings_deleteAlias">
<div className="mx_RoomSettings_deleteAlias mx_filterFlipColor">
{ deleteButton }
</div>
</div>
@ -297,7 +297,7 @@ module.exports = React.createClass({
placeholder={ "New address (e.g. #foo:" + localDomain + ")" }
blurToCancel={ false }
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"
onClick={ self.onAliasAdded.bind(self, undefined) }/>
</div>
@ -310,4 +310,4 @@ module.exports = React.createClass({
</div>
);
}
});
});

View file

@ -135,7 +135,7 @@ module.exports = React.createClass({
</div>
);
}
var boundClick = this._onColorSchemeChanged.bind(this, i)
var boundClick = this._onColorSchemeChanged.bind(this, i);
return (
<div className="mx_RoomSettings_roomColor"
key={ "room_color_" + i }

View file

@ -121,13 +121,13 @@ module.exports = React.createClass({
onChange={ this.onGlobalDisableUrlPreviewChange }
checked={ this.state.globalDisableUrlPreview } />
Disable URL previews by default for participants in this room
</label>
</label>;
}
else {
disableRoomPreviewUrls =
<label>
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
</label>
</label>;
}
return (
@ -154,4 +154,4 @@ module.exports = React.createClass({
);
}
});
});

View file

@ -93,8 +93,8 @@ module.exports = React.createClass({
}
else {
joinText = (<span>
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice')}}
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video') }}
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}}
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video'); }}
href="#">video</a>.
</span>);

View file

@ -149,13 +149,13 @@ module.exports = WithMatrixClient(React.createClass({
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
},
componentWillReceiveProps: function (nextProps) {
componentWillReceiveProps: function(nextProps) {
if (nextProps.mxEvent !== this.props.mxEvent) {
this._verifyEvent(nextProps.mxEvent);
}
},
shouldComponentUpdate: function (nextProps, nextState) {
shouldComponentUpdate: function(nextProps, nextState) {
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
return true;
}
@ -259,11 +259,11 @@ module.exports = WithMatrixClient(React.createClass({
onEditClicked: function(e) {
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
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;
ContextualMenu.createMenu(MessageContextMenu, {
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,
// rather than just HH:MM:SS.
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1)
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1);
dayAfterEvent.setHours(0);
dayAfterEvent.setMinutes(0);
dayAfterEvent.setSeconds(0);
@ -366,10 +366,11 @@ module.exports = WithMatrixClient(React.createClass({
},
onCryptoClicked: function(e) {
var EncryptedEventDialog = sdk.getComponent("dialogs.EncryptedEventDialog");
var event = this.props.mxEvent;
Modal.createDialog(EncryptedEventDialog, {
Modal.createDialogAsync((cb) => {
require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb);
}, {
event: event,
});
},
@ -465,7 +466,7 @@ module.exports = WithMatrixClient(React.createClass({
}
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;

View file

@ -60,13 +60,15 @@ module.exports = React.createClass({
},
componentDidMount: function() {
if (this.refs.description)
if (this.refs.description) {
linkifyElement(this.refs.description, linkifyMatrix.options);
}
},
componentDidUpdate: function() {
if (this.refs.description)
if (this.refs.description) {
linkifyElement(this.refs.description, linkifyMatrix.options);
}
},
componentWillUnmount: function() {
@ -116,7 +118,7 @@ module.exports = React.createClass({
if (image) {
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={ image } onClick={ this.onImageClick }/>
</div>
</div>;
}
return (

View file

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

View file

@ -65,7 +65,7 @@ module.exports = WithMatrixClient(React.createClass({
updating: 0,
devicesLoading: true,
devices: null,
}
};
},
componentWillMount: function() {
@ -203,7 +203,7 @@ module.exports = WithMatrixClient(React.createClass({
}
var cancelled = false;
this._cancelDeviceList = function() { cancelled = true; }
this._cancelDeviceList = function() { cancelled = true; };
var client = this.props.matrixClient;
var self = this;
@ -530,7 +530,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;
if(!avatarUrl) return;
@ -621,7 +621,7 @@ module.exports = WithMatrixClient(React.createClass({
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>Start new chat</i></div>
</AccessibleButton>
</AccessibleButton>;
startChat = <div>
<h3>Direct chats</h3>
@ -655,7 +655,7 @@ module.exports = WithMatrixClient(React.createClass({
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</AccessibleButton>
</AccessibleButton>;
}
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
@ -673,7 +673,7 @@ module.exports = WithMatrixClient(React.createClass({
{banButton}
{giveModButton}
</div>
</div>
</div>;
}
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/>
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.
</span>
</span>;
module.exports = React.createClass({
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.
var EntityTile = sdk.getComponent("rooms.EntityTile");
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
@ -338,8 +338,8 @@ module.exports = React.createClass({
}
memberList.push(
<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 < nextProps.member.user.getLastModifiedTime())
) {
return true
return true;
}
return false;
},

View file

@ -222,20 +222,22 @@ export default class MessageComposer extends React.Component {
</div>
);
let e2eimg, e2etitle;
let e2eImg, e2eTitle, e2eClass;
if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
// FIXME: show a /!\ if there are untrusted devices in the room...
e2eimg = 'img/e2e-verified.svg';
e2etitle = 'Encrypted room';
e2eImg = 'img/e2e-verified.svg';
e2eTitle = 'Encrypted room';
e2eClass = 'mx_MessageComposer_e2eIcon';
} else {
e2eimg = 'img/e2e-unencrypted.svg';
e2etitle = 'Unencrypted room';
e2eImg = 'img/e2e-unencrypted.svg';
e2eTitle = 'Unencrypted room';
e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor';
}
controls.push(
<img key="e2eIcon" className="mx_MessageComposer_e2eIcon" src={e2eimg} width="12" height="12"
alt={e2etitle} title={e2etitle}
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
alt={e2eTitle} title={e2eTitle}
/>
);
var callButton, videoCallButton, hangupButton;
@ -331,6 +333,7 @@ export default class MessageComposer extends React.Component {
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
const className = classNames("mx_MessageComposer_format_button", {
mx_MessageComposer_format_button_disabled: disabled,
mx_filterFlipColor: true,
});
return <img className={className}
title={name}
@ -355,11 +358,11 @@ export default class MessageComposer extends React.Component {
<div style={{flex: 1}}></div>
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown"
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title="Hide Text Formatting Toolbar"
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel"
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
</div>
</div>: null
@ -367,7 +370,7 @@ export default class MessageComposer extends React.Component {
</div>
);
}
};
}
MessageComposer.propTypes = {
tabComplete: React.PropTypes.any,

View file

@ -443,12 +443,12 @@ export default class MessageComposerInput extends React.Component {
selection = this.state.editorState.getSelection();
let modifyFn = {
bold: text => `**${text}**`,
italic: text => `*${text}*`,
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
strike: text => `~~${text}~~`,
code: text => `\`${text}\``,
blockquote: text => text.split('\n').map(line => `> ${line}\n`).join(''),
'bold': text => `**${text}**`,
'italic': text => `*${text}*`,
'underline': text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
'strike': text => `~~${text}~~`,
'code': text => `\`${text}\``,
'blockquote': 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(''),
}[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);
}
if (newState != null) {
this.setEditorState(newState);
@ -523,7 +524,9 @@ export default class MessageComposerInput extends React.Component {
);
} else {
const md = new Markdown(contentText);
if (!md.isPlainText()) {
if (md.isPlainText()) {
contentText = md.toPlaintext();
} else {
contentHTML = md.toHTML();
}
}
@ -663,7 +666,7 @@ export default class MessageComposerInput extends React.Component {
const blockName = {
'code-block': 'code',
blockquote: 'quote',
'blockquote': 'quote',
'unordered-list-item': 'bullet',
'ordered-list-item': 'numbullet',
};
@ -716,7 +719,7 @@ export default class MessageComposerInput extends React.Component {
selection={selection} />
</div>
<div className={className}>
<img className="mx_MessageComposer_input_markdownIndicator"
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
onMouseDown={this.onMarkdownToggleClicked}
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
@ -738,7 +741,7 @@ export default class MessageComposerInput extends React.Component {
</div>
);
}
};
}
MessageComposerInput.propTypes = {
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) {
var input = this.refs.textarea.value;
if (input.length === 0) {
@ -331,6 +331,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
}
else {
const contentText = mdown.toPlaintext();
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendEmoteMessage(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() {
return {
leftOffset: 0,
}
};
},
getInitialState: function() {
@ -81,7 +81,7 @@ module.exports = React.createClass({
// position.
return {
suppressDisplay: !this.props.suppressAnimation,
}
};
},
componentWillUnmount: function() {

View file

@ -183,8 +183,8 @@ module.exports = React.createClass({
'm.room.name', user_id
);
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>
cancel_button = <AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </AccessibleButton>
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
cancel_button = <AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </AccessibleButton>;
}
if (this.props.saving) {
@ -194,7 +194,7 @@ module.exports = React.createClass({
if (can_set_room_name) {
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
}
else {
var searchStatus;
@ -233,7 +233,7 @@ module.exports = React.createClass({
if (can_set_room_topic) {
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 {
var topic;
if (this.props.room) {
@ -302,7 +302,11 @@ module.exports = React.createClass({
rightPanel_buttons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
<<<<<<< HEAD
</AccessibleButton>
=======
</div>;
>>>>>>> origin/develop
}
var right_row;

View file

@ -46,7 +46,7 @@ module.exports = React.createClass({
isLoadingLeftRooms: false,
lists: {},
incomingCall: null,
}
};
},
componentWillMount: function() {
@ -338,7 +338,7 @@ module.exports = React.createClass({
// as this is used to calculate the CSS fixed top position for the stickies
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
top = (top < scrollAreaOffset) ? scrollAreaOffset : top;
// 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 stickyHeader = sticky.childNodes[0];
var topStuckHeight = stickyHeight * i;
var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i)
var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i);
if (self.scrollAreaSufficient && stickyPosition < (scrollArea.scrollTop + topStuckHeight)) {
// Top stickies
@ -520,7 +520,7 @@ module.exports = React.createClass({
collapsed={ self.props.collapsed }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
onShowMoreRooms={ self.onShowMoreRooms } />;
}
}) }

View file

@ -58,7 +58,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
busy: false
}
};
},
componentWillMount: function() {
@ -96,7 +96,7 @@ module.exports = React.createClass({
emailMatchBlock = <div className="error">
Unable to ascertain that the address this invite was
sent to matches one associated with your account.
</div>
</div>;
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) {
emailMatchBlock =
<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/>
You may wish to login with a different account, or add this email to this account.
</div>
</div>
</div>;
}
}
joinBlock = (

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