diff --git a/README.md b/README.md
index c834e0137..91109f968 100644
--- a/README.md
+++ b/README.md
@@ -36,9 +36,10 @@ about them:
2. `cd matrix-react-sdk`
3. `git checkout develop`
4. `npm install`
-5. `npm start` (to start the dev rebuilder)
-6. `cd ../vector-web`
-7. Link the react sdk package into the example:
+5. `npm run build`
+6. `npm start` (to start the dev rebuilder)
+7. `cd ../vector-web`
+8. Link the react sdk package into the example:
`npm link path/to/your/react/sdk`
Similarly, you may need to `npm link path/to/your/js/sdk` in your `matrix-react-sdk`
@@ -53,6 +54,6 @@ about "Cannot resolve module 'source-map-loader'" due to shortcomings in webpack
Deployment
==========
-Just run `npm build` and then mount the `vector` directory on your webserver to
+Just run `npm run build` and then mount the `vector` directory on your webserver to
actually serve up the app, which is entirely static content.
diff --git a/package.json b/package.json
index eb9c3aff9..035ccbc17 100644
--- a/package.json
+++ b/package.json
@@ -27,14 +27,20 @@
"classnames": "^2.1.2",
"filesize": "^3.1.2",
"flux": "~2.0.3",
+ "gfm.css": "^1.1.1",
+ "highlight.js": "^8.9.1",
"linkifyjs": "^2.0.0-beta.4",
+ "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop",
+ "matrix-react-sdk": "https://github.com/matrix-org/matrix-react-sdk.git#develop",
"modernizr": "^3.1.0",
- "matrix-js-sdk": "^0.3.0",
- "matrix-react-sdk": "^0.0.2",
"q": "^1.4.1",
- "react": "^0.13.3",
- "react-loader": "^1.4.0",
- "sanitize-html": "^1.11.1"
+ "react": "^0.14.2",
+ "react-dnd": "^2.0.2",
+ "react-dnd-html5-backend": "^2.0.0",
+ "react-dom": "^0.14.2",
+ "react-gemini-scrollbar": "^2.0.1",
+ "sanitize-html": "^1.0.0",
+ "velocity-animate": "^1.2.3"
},
"devDependencies": {
"babel": "^5.8.23",
@@ -46,6 +52,7 @@
"parallelshell": "^1.2.0",
"rimraf": "^2.4.3",
"source-map-loader": "^0.1.5",
- "uglifycss": "0.0.15"
+ "uglifycss": "0.0.15",
+ "webpack": "^1.12.6"
}
}
diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js
index 7865e45a7..a7b1849e1 100644
--- a/src/ContextualMenu.js
+++ b/src/ContextualMenu.js
@@ -18,6 +18,7 @@ limitations under the License.
'use strict';
var React = require('react');
+var ReactDOM = require('react-dom');
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@@ -42,7 +43,7 @@ module.exports = {
var self = this;
var closeMenu = function() {
- React.unmountComponentAtNode(self.getOrCreateContainer());
+ ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) props.onFinished.apply(null, arguments);
};
@@ -74,7 +75,7 @@ module.exports = {
);
- React.render(menu, this.getOrCreateContainer());
+ ReactDOM.render(menu, this.getOrCreateContainer());
return {close: closeMenu};
},
diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
new file mode 100644
index 000000000..824f59ab2
--- /dev/null
+++ b/src/HtmlUtils.js
@@ -0,0 +1,108 @@
+/*
+Copyright 2015 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+var React = require('react');
+var sanitizeHtml = require('sanitize-html');
+var highlight = require('highlight.js');
+
+var sanitizeHtmlParams = {
+ allowedTags: [
+ 'font', // custom to matrix. deliberately no h1/h2 to stop people shouting.
+ 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
+ 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
+ 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
+ ],
+ allowedAttributes: {
+ // custom ones first:
+ font: [ 'color' ], // custom to matrix
+ a: [ 'href', 'name', 'target' ], // remote target: custom to matrix
+ // We don't currently allow img itself by default, but this
+ // would make sense if we did
+ 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' ],
+ // URL schemes we permit
+ allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
+ allowedSchemesByTag: {},
+
+ transformTags: { // custom to matrix
+ // add blank targets to all hyperlinks
+ 'a': sanitizeHtml.simpleTransform('a', { target: '_blank'} )
+ },
+};
+
+module.exports = {
+ bodyToHtml: function(content, searchTerm) {
+ var originalBody = content.body;
+ var body;
+
+ if (searchTerm) {
+ var lastOffset = 0;
+ var bodyList = [];
+ var k = 0;
+ var offset;
+
+ // XXX: rather than searching for the search term in the body,
+ // we should be looking at the match delimiters returned by the FTS engine
+ if (content.format === "org.matrix.custom.html") {
+
+ var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
+ var safeSearchTerm = sanitizeHtml(searchTerm, sanitizeHtmlParams);
+ while ((offset = safeBody.indexOf(safeSearchTerm, lastOffset)) >= 0) {
+ // FIXME: we need to apply the search highlighting to only the text elements of HTML, which means
+ // hooking into the sanitizer parser rather than treating it as a string. Otherwise
+ // the act of highlighting a or whatever will break the HTML badly.
+ bodyList.push();
+ bodyList.push();
+ lastOffset = offset + safeSearchTerm.length;
+ }
+ bodyList.push();
+ }
+ else {
+ while ((offset = originalBody.indexOf(searchTerm, lastOffset)) >= 0) {
+ bodyList.push({ originalBody.substring(lastOffset, offset) });
+ bodyList.push({ searchTerm });
+ lastOffset = offset + searchTerm.length;
+ }
+ bodyList.push({ originalBody.substring(lastOffset) });
+ }
+ body = bodyList;
+ }
+ else {
+ if (content.format === "org.matrix.custom.html") {
+ var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
+ body = ;
+ }
+ else {
+ body = originalBody;
+ }
+ }
+
+ return body;
+ },
+
+ highlightDom: function(element) {
+ var blocks = element.getElementsByTagName("code");
+ for (var i = 0; i < blocks.length; i++) {
+ highlight.highlightBlock(blocks[i]);
+ }
+ },
+
+}
+
diff --git a/src/Resend.js b/src/Resend.js
new file mode 100644
index 000000000..52b7c9365
--- /dev/null
+++ b/src/Resend.js
@@ -0,0 +1,24 @@
+var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
+var dis = require('matrix-react-sdk/lib/dispatcher');
+
+module.exports = {
+ resend: function(event) {
+ MatrixClientPeg.get().resendEvent(
+ event, MatrixClientPeg.get().getRoom(event.getRoomId())
+ ).done(function() {
+ dis.dispatch({
+ action: 'message_sent',
+ event: event
+ });
+ }, function() {
+ dis.dispatch({
+ action: 'message_send_failed',
+ event: event
+ });
+ });
+ dis.dispatch({
+ action: 'message_resend_started',
+ event: event
+ });
+ },
+};
\ No newline at end of file
diff --git a/src/Velociraptor.js b/src/Velociraptor.js
new file mode 100644
index 000000000..d973a17f7
--- /dev/null
+++ b/src/Velociraptor.js
@@ -0,0 +1,113 @@
+var React = require('react');
+var ReactDom = require('react-dom');
+var Velocity = require('velocity-animate');
+
+/**
+ * The Velociraptor contains components and animates transitions with velocity.
+ * It will only pick up direct changes to properties ('left', currently), and so
+ * will not work for animating positional changes where the position is implicit
+ * from DOM order. This makes it a lot simpler and lighter: if you need fully
+ * automatic positional animation, look at react-shuffle or similar libraries.
+ */
+module.exports = React.createClass({
+ displayName: 'Velociraptor',
+
+ propTypes: {
+ children: React.PropTypes.array,
+ transition: React.PropTypes.object,
+ container: React.PropTypes.string
+ },
+
+ componentWillMount: function() {
+ this.children = {};
+ this.nodes = {};
+ var self = this;
+ React.Children.map(this.props.children, function(c) {
+ self.children[c.key] = c;
+ });
+ },
+
+ componentWillReceiveProps: function(nextProps) {
+ var self = this;
+ var oldChildren = this.children;
+ this.children = {};
+ React.Children.map(nextProps.children, function(c) {
+ if (oldChildren[c.key]) {
+ var old = oldChildren[c.key];
+ var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
+
+ if (oldNode.style.left != c.props.style.left) {
+ Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
+ // special case visibility because it's nonsensical to animate an invisible element
+ // so we always hidden->visible pre-transition and visible->hidden after
+ if (oldNode.style.visibility == 'visible' && c.props.style.visibility == 'hidden') {
+ oldNode.style.visibility = c.props.style.visibility;
+ }
+ });
+ if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
+ oldNode.style.visibility = c.props.style.visibility;
+ }
+ //console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
+ }
+ self.children[c.key] = old;
+ } else {
+ // new element. If it has a startStyle, use that as the style and go through
+ // the enter animations
+ var newProps = {
+ ref: self.collectNode.bind(self, c.key)
+ };
+ if (c.props.startStyle && Object.keys(c.props.startStyle).length) {
+ var startStyle = c.props.startStyle;
+ if (Array.isArray(startStyle)) {
+ startStyle = startStyle[0];
+ }
+ newProps._restingStyle = c.props.style;
+ newProps.style = startStyle;
+ //console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
+ // apply the enter animations once it's mounted
+ }
+ self.children[c.key] = React.cloneElement(c, newProps);
+ }
+ });
+ },
+
+ collectNode: function(k, node) {
+ if (
+ this.nodes[k] === undefined &&
+ node.props.startStyle &&
+ Object.keys(node.props.startStyle).length
+ ) {
+ var domNode = ReactDom.findDOMNode(node);
+ var startStyles = node.props.startStyle;
+ var transitionOpts = node.props.enterTransitionOpts;
+ if (!Array.isArray(startStyles)) {
+ startStyles = [ startStyles ];
+ transitionOpts = [ transitionOpts ];
+ }
+ // 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) {
+ Velocity(domNode, startStyles[i], transitionOpts[i-1]);
+ //console.log("start: "+JSON.stringify(startStyles[i]));
+ }
+ // and then we animate to the resting state
+ Velocity(domNode, node.props._restingStyle, transitionOpts[i-1]);
+ //console.log("enter: "+JSON.stringify(node.props._restingStyle));
+ }
+ this.nodes[k] = node;
+ },
+
+ render: function() {
+ var self = this;
+ var childList = Object.keys(this.children).map(function(k) {
+ return React.cloneElement(self.children[k], {
+ ref: self.collectNode.bind(self, self.children[k].key)
+ });
+ });
+ return (
+
+ {childList}
+
+ );
+ },
+});
diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js
new file mode 100644
index 000000000..c85aa254f
--- /dev/null
+++ b/src/VelocityBounce.js
@@ -0,0 +1,15 @@
+var Velocity = require('velocity-animate');
+
+// courtesy of https://github.com/julianshapiro/velocity/issues/283
+// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
+function bounce( p ) {
+ var pow2,
+ bounce = 4;
+
+ while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
+ 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);
+}
diff --git a/src/components/login/Login.js b/src/components/login/Login.js
new file mode 100644
index 000000000..80d0314fc
--- /dev/null
+++ b/src/components/login/Login.js
@@ -0,0 +1,199 @@
+/*
+Copyright 2015 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+var React = require('react');
+var ReactDOM = require('react-dom');
+var sdk = require('matrix-react-sdk');
+var Signup = require("matrix-react-sdk/lib/Signup");
+var PasswordLogin = require("matrix-react-sdk/lib/components/login/PasswordLogin");
+var CasLogin = require("matrix-react-sdk/lib/components/login/CasLogin");
+var ServerConfig = require("./ServerConfig");
+
+/**
+ * A wire component which glues together login UI components and Signup logic
+ */
+module.exports = React.createClass({displayName: 'Login',
+ propTypes: {
+ onLoggedIn: React.PropTypes.func.isRequired,
+ homeserverUrl: React.PropTypes.string,
+ identityServerUrl: React.PropTypes.string,
+ // login shouldn't know or care how registration is done.
+ onRegisterClick: React.PropTypes.func.isRequired
+ },
+
+ getDefaultProps: function() {
+ return {
+ homeserverUrl: 'https://matrix.org/',
+ identityServerUrl: 'https://vector.im'
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ busy: false,
+ errorText: null,
+ enteredHomeserverUrl: this.props.homeserverUrl,
+ enteredIdentityServerUrl: this.props.identityServerUrl
+ };
+ },
+
+ componentWillMount: function() {
+ this._initLoginLogic();
+ },
+
+ onPasswordLogin: function(username, password) {
+ var self = this;
+ self.setState({
+ busy: true
+ });
+
+ this._loginLogic.loginViaPassword(username, password).then(function(data) {
+ self.props.onLoggedIn(data);
+ }, function(error) {
+ self._setErrorTextFromError(error);
+ }).finally(function() {
+ self.setState({
+ busy: false
+ });
+ });
+ },
+
+ onHsUrlChanged: function(newHsUrl) {
+ this._initLoginLogic(newHsUrl);
+ },
+
+ onIsUrlChanged: function(newIsUrl) {
+ this._initLoginLogic(null, newIsUrl);
+ },
+
+ _initLoginLogic: function(hsUrl, isUrl) {
+ var self = this;
+ hsUrl = hsUrl || this.state.enteredHomeserverUrl;
+ isUrl = isUrl || this.state.enteredIdentityServerUrl;
+
+ var loginLogic = new Signup.Login(hsUrl, isUrl);
+ this._loginLogic = loginLogic;
+
+ loginLogic.getFlows().then(function(flows) {
+ // old behaviour was to always use the first flow without presenting
+ // options. This works in most cases (we don't have a UI for multiple
+ // logins so let's skip that for now).
+ loginLogic.chooseFlow(0);
+ }, function(err) {
+ self._setErrorTextFromError(err);
+ }).finally(function() {
+ self.setState({
+ busy: false
+ });
+ });
+
+ this.setState({
+ enteredHomeserverUrl: hsUrl,
+ enteredIdentityServerUrl: isUrl,
+ busy: true,
+ errorText: null // reset err messages
+ });
+ },
+
+ _getCurrentFlowStep: function() {
+ return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null
+ },
+
+ _setErrorTextFromError: function(err) {
+ if (err.friendlyText) {
+ this.setState({
+ errorText: err.friendlyText
+ });
+ return;
+ }
+
+ var errCode = err.errcode;
+ if (!errCode && err.httpStatus) {
+ errCode = "HTTP " + err.httpStatus;
+ }
+ this.setState({
+ errorText: (
+ "Error: Problem communicating with the given homeserver " +
+ (errCode ? "(" + errCode + ")" : "")
+ )
+ });
+ },
+
+ componentForStep: function(step) {
+ switch (step) {
+ case 'm.login.password':
+ return (
+
Room | Alias | Members |
---|---|---|
Room | Alias | Members |