fix conflicts

This commit is contained in:
Matthew Hodgson 2015-11-24 16:05:58 +00:00
commit f9040e08ce
82 changed files with 2870 additions and 1037 deletions

View File

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

View File

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

View File

@ -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 = {
</div>
);
React.render(menu, this.getOrCreateContainer());
ReactDOM.render(menu, this.getOrCreateContainer());
return {close: closeMenu};
},

108
src/HtmlUtils.js Normal file
View File

@ -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 <b/> or whatever will break the HTML badly.
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset, offset) }} />);
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSearchTerm }} className="mx_MessageTile_searchHighlight" />);
lastOffset = offset + safeSearchTerm.length;
}
bodyList.push(<span className="markdown-body" key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset) }} />);
}
else {
while ((offset = originalBody.indexOf(searchTerm, lastOffset)) >= 0) {
bodyList.push(<span key={ k++ } >{ originalBody.substring(lastOffset, offset) }</span>);
bodyList.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ searchTerm }</span>);
lastOffset = offset + searchTerm.length;
}
bodyList.push(<span key={ k++ }>{ originalBody.substring(lastOffset) }</span>);
}
body = bodyList;
}
else {
if (content.format === "org.matrix.custom.html") {
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
body = <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
}
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]);
}
},
}

24
src/Resend.js Normal file
View File

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

113
src/Velociraptor.js Normal file
View File

@ -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 (
<span>
{childList}
</span>
);
},
});

15
src/VelocityBounce.js Normal file
View File

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

View File

@ -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 (
<PasswordLogin onSubmit={this.onPasswordLogin} />
);
case 'm.login.cas':
return (
<CasLogin />
);
default:
if (!step) {
return;
}
return (
<div>
Sorry, this homeserver is using a login which is not
recognised by Vector ({step})
</div>
);
}
},
render: function() {
var Loader = sdk.getComponent("atoms.Spinner");
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
return (
<div className="mx_Login">
<div className="mx_Login_box">
<div className="mx_Login_logo">
<img src="img/logo.png" width="249" height="78" alt="vector"/>
</div>
<div>
<h2>Sign in</h2>
{ this.componentForStep(this._getCurrentFlowStep()) }
<ServerConfig ref="serverConfig"
withToggleButton={true}
defaultHsUrl={this.props.homeserverUrl}
defaultIsUrl={this.props.identityServerUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={1000}/>
<div className="mx_Login_error">
{ loader }
{ this.state.errorText }
</div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account
</a>
<br/>
<div className="mx_Login_links">
<a href="https://medium.com/@Vector">blog</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
<a href="https://twitter.com/@VectorCo">twitter</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
<a href="https://github.com/vector-im/vector-web">github</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
<a href="https://matrix.org">powered by Matrix</a>
</div>
</div>
</div>
</div>
);
}
});

View File

@ -0,0 +1,81 @@
/*
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 sdk = require('matrix-react-sdk');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
module.exports = React.createClass({
displayName: 'PostRegistration',
propTypes: {
onComplete: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
avatarUrl: null,
errorString: null,
busy: false
};
},
componentWillMount: function() {
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
// the URL to be passed to you (because it's also used for room avatars).
var cli = MatrixClientPeg.get();
this.setState({busy: true});
var self = this;
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
self.setState({
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
busy: false
});
}, function(error) {
self.setState({
errorString: "Failed to fetch avatar URL",
busy: false
});
});
},
render: function() {
var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName');
var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar');
return (
<div className="mx_Login">
<div className="mx_Login_box">
<div className="mx_Login_logo">
<img src="img/logo.png" width="249" height="78" alt="vector"/>
</div>
<div className="mx_Login_profile">
Set a display name:
<ChangeDisplayName />
Upload an avatar:
<ChangeAvatar
initialAvatarUrl={this.state.avatarUrl} />
<button onClick={this.props.onComplete}>Continue</button>
{this.state.errorString}
</div>
</div>
</div>
);
}
});

View File

@ -0,0 +1,247 @@
/*
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 sdk = require('matrix-react-sdk');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var dis = require('matrix-react-sdk/lib/dispatcher');
var ServerConfig = require("./ServerConfig");
var RegistrationForm = require("./RegistrationForm");
var CaptchaForm = require("matrix-react-sdk/lib/components/login/CaptchaForm");
var Signup = require("matrix-react-sdk/lib/Signup");
var MIN_PASSWORD_LENGTH = 6;
module.exports = React.createClass({
displayName: 'Registration',
propTypes: {
onLoggedIn: React.PropTypes.func.isRequired,
clientSecret: React.PropTypes.string,
sessionId: React.PropTypes.string,
registrationUrl: React.PropTypes.string,
idSid: React.PropTypes.string,
hsUrl: React.PropTypes.string,
isUrl: React.PropTypes.string,
// registration shouldn't know or care how login is done.
onLoginClick: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
busy: false,
errorText: null,
enteredHomeserverUrl: this.props.hsUrl,
enteredIdentityServerUrl: this.props.isUrl
};
},
componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction);
// attach this to the instance rather than this.state since it isn't UI
this.registerLogic = new Signup.Register(
this.props.hsUrl, this.props.isUrl
);
this.registerLogic.setClientSecret(this.props.clientSecret);
this.registerLogic.setSessionId(this.props.sessionId);
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
this.registerLogic.setIdSid(this.props.idSid);
this.registerLogic.recheckState();
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
componentDidMount: function() {
// may have already done an HTTP hit (e.g. redirect from an email) so
// check for any pending response
var promise = this.registerLogic.getPromise();
if (promise) {
this.onProcessingRegistration(promise);
}
},
onHsUrlChanged: function(newHsUrl) {
this.registerLogic.setHomeserverUrl(newHsUrl);
},
onIsUrlChanged: function(newIsUrl) {
this.registerLogic.setIdentityServerUrl(newIsUrl);
},
onAction: function(payload) {
if (payload.action !== "registration_step_update") {
return;
}
this.forceUpdate(); // registration state has changed.
},
onFormSubmit: function(formVals) {
var self = this;
this.setState({
errorText: "",
busy: true
});
this.onProcessingRegistration(this.registerLogic.register(formVals));
},
// Promise is resolved when the registration process is FULLY COMPLETE
onProcessingRegistration: function(promise) {
var self = this;
promise.done(function(response) {
if (!response || !response.access_token) {
console.warn(
"FIXME: Register fulfilled without a final response, " +
"did you break the promise chain?"
);
// no matter, we'll grab it direct
response = self.registerLogic.getCredentials();
}
if (!response || !response.user_id || !response.access_token) {
console.error("Final response is missing keys.");
self.setState({
errorText: "There was a problem processing the response."
});
return;
}
self.props.onLoggedIn({
userId: response.user_id,
homeserverUrl: self.registerLogic.getHomeserverUrl(),
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token
});
self.setState({
busy: false
});
}, function(err) {
if (err.message) {
self.setState({
errorText: err.message
});
}
self.setState({
busy: false
});
console.log(err);
});
},
onFormValidationFailed: function(errCode) {
var errMsg;
switch (errCode) {
case "RegistrationForm.ERR_PASSWORD_MISSING":
errMsg = "Missing password.";
break;
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
errMsg = "Passwords don't match.";
break;
case "RegistrationForm.ERR_PASSWORD_LENGTH":
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
break;
default:
console.error("Unknown error code: %s", errCode);
errMsg = "An unknown error occurred.";
break;
}
this.setState({
errorText: errMsg
});
},
onCaptchaLoaded: function(divIdName) {
this.registerLogic.tellStage("m.login.recaptcha", {
divId: divIdName
});
this.setState({
busy: false // requires user input
});
},
_getRegisterContentJsx: function() {
var currStep = this.registerLogic.getStep();
var registerStep;
switch (currStep) {
case "Register.COMPLETE":
break; // NOP
case "Register.START":
case "Register.STEP_m.login.dummy":
registerStep = (
<RegistrationForm
showEmail={true}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit} />
);
break;
case "Register.STEP_m.login.email.identity":
registerStep = (
<div>
Please check your email to continue registration.
</div>
);
break;
case "Register.STEP_m.login.recaptcha":
registerStep = (
<CaptchaForm onCaptchaLoaded={this.onCaptchaLoaded} />
);
break;
default:
console.error("Unknown register state: %s", currStep);
break;
}
var busySpinner;
if (this.state.busy) {
var Spinner = sdk.getComponent("atoms.Spinner");
busySpinner = (
<Spinner />
);
}
return (
<div>
<h2>Create an account</h2>
{registerStep}
<div className="mx_Login_error">{this.state.errorText}</div>
{busySpinner}
<ServerConfig ref="serverConfig"
withToggleButton={true}
defaultHsUrl={this.state.enteredHomeserverUrl}
defaultIsUrl={this.state.enteredIdentityServerUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={1000} />
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
I already have an account
</a>
</div>
);
},
render: function() {
return (
<div className="mx_Login">
<div className="mx_Login_box">
<div className="mx_Login_logo">
<img src="img/logo.png" width="249" height="78" alt="vector"/>
</div>
{this._getRegisterContentJsx()}
</div>
</div>
);
}
});

View File

@ -0,0 +1,126 @@
/*
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 sdk = require('matrix-react-sdk')
/**
* A pure UI component which displays a registration form.
*/
module.exports = React.createClass({
displayName: 'RegistrationForm',
propTypes: {
defaultEmail: React.PropTypes.string,
defaultUsername: React.PropTypes.string,
showEmail: React.PropTypes.bool,
minPasswordLength: React.PropTypes.number,
onError: React.PropTypes.func,
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
},
getDefaultProps: function() {
return {
showEmail: false,
minPasswordLength: 6,
onError: function(e) {
console.error(e);
}
};
},
getInitialState: function() {
return {
email: this.props.defaultEmail,
username: this.props.defaultUsername,
password: null,
passwordConfirm: null
};
},
onSubmit: function(ev) {
ev.preventDefault();
var pwd1 = this.refs.password.value.trim();
var pwd2 = this.refs.passwordConfirm.value.trim()
var errCode;
if (!pwd1 || !pwd2) {
errCode = "RegistrationForm.ERR_PASSWORD_MISSING";
}
else if (pwd1 !== pwd2) {
errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH";
}
else if (pwd1.length < this.props.minPasswordLength) {
errCode = "RegistrationForm.ERR_PASSWORD_LENGTH";
}
if (errCode) {
this.props.onError(errCode);
return;
}
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim(),
password: pwd1,
email: this.refs.email.value.trim()
});
if (promise) {
ev.target.disabled = true;
promise.finally(function() {
ev.target.disabled = false;
});
}
},
render: function() {
var emailSection, registerButton;
if (this.props.showEmail) {
emailSection = (
<input className="mx_Login_field" type="text" ref="email"
autoFocus={true} placeholder="Email address"
defaultValue={this.state.email} />
);
}
if (this.props.onRegisterClick) {
registerButton = (
<input className="mx_Login_submit" type="submit" value="Register" />
);
}
return (
<div>
<form onSubmit={this.onSubmit}>
{emailSection}
<br />
<input className="mx_Login_field" type="text" ref="username"
placeholder="User name" defaultValue={this.state.username} />
<br />
<input className="mx_Login_field" type="password" ref="password"
placeholder="Password" defaultValue={this.state.password} />
<br />
<input className="mx_Login_field" type="password" ref="passwordConfirm"
placeholder="Confirm password"
defaultValue={this.state.passwordConfirm} />
<br />
{registerButton}
</form>
</div>
);
}
});

View File

@ -0,0 +1,161 @@
/*
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 Modal = require('matrix-react-sdk/lib/Modal');
var sdk = require('matrix-react-sdk')
/**
* A pure UI component which displays the HS and IS to use.
*/
module.exports = React.createClass({
displayName: 'ServerConfig',
propTypes: {
onHsUrlChanged: React.PropTypes.func,
onIsUrlChanged: React.PropTypes.func,
defaultHsUrl: React.PropTypes.string,
defaultIsUrl: React.PropTypes.string,
withToggleButton: React.PropTypes.bool,
delayTimeMs: React.PropTypes.number // time to wait before invoking onChanged
},
getDefaultProps: function() {
return {
onHsUrlChanged: function() {},
onIsUrlChanged: function() {},
withToggleButton: false,
delayTimeMs: 0
};
},
getInitialState: function() {
return {
hs_url: this.props.defaultHsUrl,
is_url: this.props.defaultIsUrl,
original_hs_url: this.props.defaultHsUrl,
original_is_url: this.props.defaultIsUrl,
// no toggle button = show, toggle button = hide
configVisible: !this.props.withToggleButton
}
},
onHomeserverChanged: function(ev) {
this.setState({hs_url: ev.target.value}, function() {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
this.props.onHsUrlChanged(this.state.hs_url);
});
});
},
onIdentityServerChanged: function(ev) {
this.setState({is_url: ev.target.value}, function() {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
this.props.onIsUrlChanged(this.state.is_url);
});
});
},
_waitThenInvoke: function(existingTimeoutId, fn) {
if (existingTimeoutId) {
clearTimeout(existingTimeoutId);
}
return setTimeout(fn.bind(this), this.props.delayTimeMs);
},
getHsUrl: function() {
return this.state.hs_url;
},
getIsUrl: function() {
return this.state.is_url;
},
onServerConfigVisibleChange: function(ev) {
this.setState({
configVisible: ev.target.checked
});
},
showHelpPopup: function() {
var ErrorDialog = sdk.getComponent('organisms.ErrorDialog');
Modal.createDialog(ErrorDialog, {
title: 'Custom Server Options',
description: <span>
You can use the custom server options to log into other Matrix
servers by specifying a different Home server URL.
<br/>
This allows you to use Vector with an existing Matrix account on
a different Home server.
<br/>
<br/>
You can also set a custom Identity server but this will affect
people&#39;s ability to find you if you use a server in a group other
than the main Matrix.org group.
</span>,
button: "Dismiss",
focus: true
});
},
render: function() {
var serverConfigStyle = {};
serverConfigStyle.display = this.state.configVisible ? 'block' : 'none';
var toggleButton;
if (this.props.withToggleButton) {
toggleButton = (
<div>
<input className="mx_Login_checkbox" id="advanced" type="checkbox"
checked={this.state.configVisible}
onChange={this.onServerConfigVisibleChange} />
<label className="mx_Login_label" htmlFor="advanced">
Use custom server options (advanced)
</label>
</div>
);
}
return (
<div>
{toggleButton}
<div style={serverConfigStyle}>
<div className="mx_ServerConfig">
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
Home server URL
</label>
<input className="mx_Login_field" id="hsurl" type="text"
placeholder={this.state.original_hs_url}
value={this.state.hs_url}
onChange={this.onHomeserverChanged} />
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
Identity server URL
</label>
<input className="mx_Login_field" id="isurl" type="text"
placeholder={this.state.original_is_url}
value={this.state.is_url}
onChange={this.onIdentityServerChanged} />
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
What does this mean?
</a>
</div>
</div>
</div>
);
}
});

View File

@ -17,22 +17,31 @@ limitations under the License.
'use strict';
var React = require("react");
var ReactDOM = require("react-dom");
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
var RoomListSorter = require("matrix-react-sdk/lib/RoomListSorter");
var dis = require("matrix-react-sdk/lib/dispatcher");
var sdk = require('matrix-react-sdk');
var VectorConferenceHandler = require("../../modules/VectorConferenceHandler");
var CallHandler = require("matrix-react-sdk/lib/CallHandler");
var HIDE_CONFERENCE_CHANS = true;
module.exports = {
getInitialState: function() {
return {
activityMap: null,
lists: {},
}
},
componentWillMount: function() {
var cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom);
cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
@ -47,11 +56,6 @@ module.exports = {
onAction: function(payload) {
switch (payload.action) {
// listen for call state changes to prod the render method, which
// may hide the global CallView if the call it is tracking is dead
case 'call_state':
this._recheckCallElement(this.props.selectedRoom);
break;
case 'view_tooltip':
this.tooltip = payload.tooltip;
this._repositionTooltip();
@ -72,7 +76,6 @@ module.exports = {
componentWillReceiveProps: function(newProps) {
this.state.activityMap[newProps.selectedRoom] = undefined;
this._recheckCallElement(newProps.selectedRoom);
this.setState({
activityMap: this.state.activityMap
});
@ -85,30 +88,43 @@ module.exports = {
onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (toStartOfTimeline) return;
var newState = this.getRoomLists();
var hl = 0;
if (
room.roomId != this.props.selectedRoom &&
ev.getSender() != MatrixClientPeg.get().credentials.userId)
{
var hl = 1;
// don't mark rooms as unread for just member changes
if (ev.getType() != "m.room.member") {
hl = 1;
}
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.tweaks && actions.tweaks.highlight) {
hl = 2;
}
}
if (hl > 0) {
var newState = this.getRoomLists();
// obviously this won't deep copy but this shouldn't be necessary
var amap = this.state.activityMap;
amap[room.roomId] = Math.max(amap[room.roomId] || 0, hl);
newState.activityMap = amap;
}
this.setState(newState);
}
},
onRoomName: function(room) {
this.refreshRoomList();
},
onRoomTags: function(event, room) {
this.refreshRoomList();
},
onRoomStateEvents: function(ev, state) {
setTimeout(this.refreshRoomList, 0);
},
@ -117,26 +133,36 @@ module.exports = {
setTimeout(this.refreshRoomList, 0);
},
refreshRoomList: function() {
// TODO: rather than bluntly regenerating and re-sorting everything
// every time we see any kind of room change from the JS SDK
// we could do incremental updates on our copy of the state
// based on the room which has actually changed. This would stop
// us re-rendering all the sublists every time anything changes anywhere
// in the state of the client.
this.setState(this.getRoomLists());
},
getRoomLists: function() {
var s = {};
var inviteList = [];
s.roomList = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms().filter(function(room) {
var s = { lists: {} };
s.lists["m.invite"] = [];
s.lists["m.favourite"] = [];
s.lists["m.recent"] = [];
s.lists["m.lowpriority"] = [];
s.lists["m.archived"] = [];
MatrixClientPeg.get().getRooms().forEach(function(room) {
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && me.membership == "invite") {
inviteList.push(room);
return false;
s.lists["m.invite"].push(room);
}
else {
var shouldShowRoom = (
me && (me.membership == "join")
);
// hiding conf rooms only ever toggles shouldShowRoom to false
if (shouldShowRoom && HIDE_CONFERENCE_CHANS) {
// we want to hide the 1:1 conf<->user room and not the group chat
@ -151,48 +177,34 @@ module.exports = {
}
}
}
return shouldShowRoom;
})
);
s.inviteList = RoomListSorter.mostRecentActivityFirst(inviteList);
return s;
},
_recheckCallElement: function(selectedRoomId) {
// if we aren't viewing a room with an ongoing call, but there is an
// active call, show the call element - we need to do this to make
// audio/video not crap out
var activeCall = CallHandler.getAnyActiveCall();
var callForRoom = CallHandler.getCallForRoom(selectedRoomId);
var showCall = (activeCall && !callForRoom);
this.setState({
show_call_element: showCall
if (shouldShowRoom) {
var tagNames = Object.keys(room.tags);
if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i];
s.lists[tagName] = s.lists[tagName] || [];
s.lists[tagNames[i]].push(room);
}
}
else {
s.lists["m.recent"].push(room);
}
}
}
});
//console.log("calculated new roomLists; m.recent = " + s.lists["m.recent"]);
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
return s;
},
_repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) {
var scroll = this.getDOMNode();
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.scrollTop) + "px";
var scroll = ReactDOM.findDOMNode(this);
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.children[2].scrollTop) + "px";
}
},
makeRoomTiles: function(list, isInvite) {
var self = this;
var RoomTile = sdk.getComponent("molecules.RoomTile");
return list.map(function(room) {
var selected = room.roomId == self.props.selectedRoom;
return (
<RoomTile
room={room}
key={room.roomId}
collapsed={self.props.collapsed}
selected={selected}
unread={self.state.activityMap[room.roomId] === 1}
highlight={self.state.activityMap[room.roomId] === 2}
isInvite={isInvite}
/>
);
});
}
};

View File

@ -17,6 +17,7 @@ limitations under the License.
var Matrix = require("matrix-js-sdk");
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
var React = require("react");
var ReactDOM = require("react-dom");
var q = require("q");
var ContentMessages = require("matrix-react-sdk/lib//ContentMessages");
var WhoIsTyping = require("matrix-react-sdk/lib/WhoIsTyping");
@ -24,6 +25,7 @@ var Modal = require("matrix-react-sdk/lib/Modal");
var sdk = require('matrix-react-sdk/lib/index');
var CallHandler = require('matrix-react-sdk/lib/CallHandler');
var VectorConferenceHandler = require('../../modules/VectorConferenceHandler');
var Resend = require("../../Resend");
var dis = require("matrix-react-sdk/lib/dispatcher");
@ -32,8 +34,9 @@ var INITIAL_SIZE = 20;
module.exports = {
getInitialState: function() {
var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null;
return {
room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null,
room: room,
messageCap: INITIAL_SIZE,
editingRoomSettings: false,
uploadingRoomSettings: false,
@ -41,6 +44,8 @@ module.exports = {
draggingFile: false,
searching: false,
searchResults: null,
syncState: MatrixClientPeg.get().getSyncState(),
hasUnsentMessages: this._hasUnsentMessages(room)
}
},
@ -48,25 +53,29 @@ module.exports = {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
this.atBottom = true;
},
componentWillUnmount: function() {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
messageWrapper.removeEventListener('drop', this.onDrop);
messageWrapper.removeEventListener('dragover', this.onDragOver);
messageWrapper.removeEventListener('dragleave', this.onDragLeaveOrEnd);
messageWrapper.removeEventListener('dragend', this.onDragLeaveOrEnd);
if (this.refs.messagePanel) {
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
messagePanel.removeEventListener('drop', this.onDrop);
messagePanel.removeEventListener('dragover', this.onDragOver);
messagePanel.removeEventListener('dragleave', this.onDragLeaveOrEnd);
messagePanel.removeEventListener('dragend', this.onDragLeaveOrEnd);
}
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange);
}
},
@ -74,6 +83,9 @@ module.exports = {
switch (payload.action) {
case 'message_send_failed':
case 'message_sent':
this.setState({
hasUnsentMessages: this._hasUnsentMessages(this.state.room)
});
case 'message_resend_started':
this.setState({
room: MatrixClientPeg.get().getRoom(this.props.roomId)
@ -88,10 +100,9 @@ module.exports = {
// Call state has changed so we may be loading video elements
// which will obscure the message log.
// scroll to bottom
var messageWrapper = this.refs.messageWrapper;
if (messageWrapper) {
messageWrapper = messageWrapper.getDOMNode();
messageWrapper.scrollTop = messageWrapper.scrollHeight;
var scrollNode = this._getScrollNode();
if (scrollNode) {
scrollNode.scrollTop = scrollNode.scrollHeight;
}
}
@ -99,9 +110,29 @@ module.exports = {
// the conf
this._updateConfCallNotification();
break;
case 'user_activity':
this.sendReadReceipt();
break;
}
},
_getScrollNode: function() {
var panel = ReactDOM.findDOMNode(this.refs.messagePanel);
if (!panel) return null;
if (panel.classList.contains('gm-prevented')) {
return panel;
} else {
return panel.children[2]; // XXX: Fragile!
}
},
onSyncStateChange: function(state) {
this.setState({
syncState: state
});
},
// MatrixRoom still showing the messages from the old room?
// Set the key to the room_id. Sadly you can no longer get at
// the key from inside the component, or we'd check this in code.
@ -111,7 +142,7 @@ module.exports = {
onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (!this.isMounted()) return;
// ignore anything that comes in whilst pagingating: we get one
// ignore anything that comes in whilst paginating: we get one
// event for each new matrix event so this would cause a huge
// number of UI updates. Just update the UI when the paginate
// call returns.
@ -122,11 +153,11 @@ module.exports = {
if (this.state.joining) return;
if (room.roomId != this.props.roomId) return;
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
var scrollNode = this._getScrollNode();
if (scrollNode) {
this.atBottom = (
messageWrapper.scrollHeight - messageWrapper.scrollTop <=
(messageWrapper.clientHeight + 150)
scrollNode.scrollHeight - scrollNode.scrollTop <=
(scrollNode.clientHeight + 150) // 150?
);
}
@ -161,6 +192,12 @@ module.exports = {
}
},
onRoomReceipt: function(receiptEvent, room) {
if (room.roomId == this.props.roomId) {
this.forceUpdate();
}
},
onRoomMemberTyping: function(ev, member) {
this.forceUpdate();
},
@ -173,6 +210,19 @@ module.exports = {
this._updateConfCallNotification();
},
_hasUnsentMessages: function(room) {
return this._getUnsentMessages(room).length > 0;
},
_getUnsentMessages: function(room) {
if (!room) { return []; }
// TODO: It would be nice if the JS SDK provided nicer constant-time
// constructs rather than O(N) (N=num msgs) on this.
return room.timeline.filter(function(ev) {
return ev.status === Matrix.EventStatus.NOT_SENT;
});
},
_updateConfCallNotification: function() {
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) return;
@ -196,15 +246,19 @@ module.exports = {
},
componentDidMount: function() {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
if (this.refs.messagePanel) {
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
messageWrapper.addEventListener('drop', this.onDrop);
messageWrapper.addEventListener('dragover', this.onDragOver);
messageWrapper.addEventListener('dragleave', this.onDragLeaveOrEnd);
messageWrapper.addEventListener('dragend', this.onDragLeaveOrEnd);
messagePanel.addEventListener('drop', this.onDrop);
messagePanel.addEventListener('dragover', this.onDragOver);
messagePanel.addEventListener('dragleave', this.onDragLeaveOrEnd);
messagePanel.addEventListener('dragend', this.onDragLeaveOrEnd);
messageWrapper.scrollTop = messageWrapper.scrollHeight;
var messageWrapperScroll = this._getScrollNode();
messageWrapperScroll.scrollTop = messageWrapperScroll.scrollHeight;
this.sendReadReceipt();
this.fillSpace();
}
@ -213,19 +267,19 @@ module.exports = {
},
componentDidUpdate: function() {
if (!this.refs.messageWrapper) return;
if (!this.refs.messagePanel) return;
var messageWrapper = this.refs.messageWrapper.getDOMNode();
var messageWrapperScroll = this._getScrollNode();
if (this.state.paginating && !this.waiting_for_paginate) {
var heightGained = messageWrapper.scrollHeight - this.oldScrollHeight;
messageWrapper.scrollTop += heightGained;
var heightGained = messageWrapperScroll.scrollHeight - this.oldScrollHeight;
messageWrapperScroll.scrollTop += heightGained;
this.oldScrollHeight = undefined;
if (!this.fillSpace()) {
this.setState({paginating: false});
}
} else if (this.atBottom) {
messageWrapper.scrollTop = messageWrapper.scrollHeight;
messageWrapperScroll.scrollTop = messageWrapperScroll.scrollHeight;
if (this.state.numUnreadMessages !== 0) {
this.setState({numUnreadMessages: 0});
}
@ -233,12 +287,12 @@ module.exports = {
},
fillSpace: function() {
if (!this.refs.messageWrapper) return;
var messageWrapper = this.refs.messageWrapper.getDOMNode();
if (messageWrapper.scrollTop < messageWrapper.clientHeight && this.state.room.oldState.paginationToken) {
if (!this.refs.messagePanel) return;
var messageWrapperScroll = this._getScrollNode();
if (messageWrapperScroll.scrollTop < messageWrapperScroll.clientHeight && this.state.room.oldState.paginationToken) {
this.setState({paginating: true});
this.oldScrollHeight = messageWrapper.scrollHeight;
this.oldScrollHeight = messageWrapperScroll.scrollHeight;
if (this.state.messageCap < this.state.room.timeline.length) {
this.waiting_for_paginate = false;
@ -265,6 +319,13 @@ module.exports = {
return false;
},
onResendAllClick: function() {
var eventsToResend = this._getUnsentMessages(this.state.room);
eventsToResend.forEach(function(event) {
Resend.resend(event);
});
},
onJoinButtonClicked: function(ev) {
var self = this;
MatrixClientPeg.get().joinRoom(this.props.roomId).then(function() {
@ -284,10 +345,10 @@ module.exports = {
},
onMessageListScroll: function(ev) {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
if (this.refs.messagePanel) {
var messageWrapperScroll = this._getScrollNode();
var wasAtBottom = this.atBottom;
this.atBottom = messageWrapper.scrollHeight - messageWrapper.scrollTop <= messageWrapper.clientHeight;
this.atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1;
if (this.atBottom && !wasAtBottom) {
this.forceUpdate(); // remove unread msg count
}
@ -350,8 +411,12 @@ module.exports = {
self.setState({
upload: undefined
});
}).done(undefined, function() {
// display error message
}).done(undefined, function(error) {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to upload file",
description: error.toString()
});
});
},
@ -377,6 +442,7 @@ module.exports = {
room_events: {
search_term: term,
filter: filter,
order_by: "recent",
event_context: {
before_limit: 1,
after_limit: 1,
@ -390,7 +456,11 @@ module.exports = {
searchResults: data,
});
}, function(error) {
// TODO: show dialog or something
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Search failed",
description: error.toString()
});
});
},
@ -408,7 +478,7 @@ module.exports = {
var eventIds = Object.keys(results);
// XXX: todo: merge overlapping results somehow?
// XXX: why doesn't searching on name work?
var resultList = eventIds.map(function(key) { return results[key]; }).sort(function(a, b) { b.rank - a.rank });
var resultList = eventIds.map(function(key) { return results[key]; }); // .sort(function(a, b) { b.rank - a.rank });
for (var i = 0; i < resultList.length; i++) {
var ts1 = resultList[i].result.origin_server_ts;
ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank}
@ -472,7 +542,7 @@ module.exports = {
}
ret.unshift(
<li key={mxEv.getId()}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li>
<li key={mxEv.getId()} ref={this._collectEventNode.bind(this, mxEv.getId())}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li>
);
if (dateSeparator) {
ret.unshift(dateSeparator);
@ -567,5 +637,58 @@ module.exports = {
uploadingRoomSettings: false,
});
}
},
_collectEventNode: function(eventId, node) {
if (this.eventNodes == undefined) this.eventNodes = {};
this.eventNodes[eventId] = node;
},
_indexForEventId(evId) {
for (var i = 0; i < this.state.room.timeline.length; ++i) {
if (evId == this.state.room.timeline[i].getId()) {
return i;
}
}
return null;
},
sendReadReceipt: function() {
if (!this.state.room) return;
var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn();
if (lastReadEventIndex === null) return;
if (lastReadEventIndex > currentReadUpToEventIndex) {
MatrixClientPeg.get().sendReadReceipt(this.state.room.timeline[lastReadEventIndex]);
}
},
_getLastDisplayedEventIndexIgnoringOwn: function() {
if (this.eventNodes === undefined) return null;
var messageWrapper = this.refs.messagePanel;
if (messageWrapper === undefined) return null;
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
for (var i = this.state.room.timeline.length-1; i >= 0; --i) {
var ev = this.state.room.timeline[i];
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
continue;
}
var node = this.eventNodes[ev.getId()];
if (!node) continue;
var boundingRect = node.getBoundingClientRect();
if (boundingRect.bottom < wrapperRect.bottom) {
return i;
}
}
return null;
}
};

View File

@ -1,58 +0,0 @@
/*
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 extend = require('matrix-react-sdk/lib/extend');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var BaseRegisterController = require('matrix-react-sdk/lib/controllers/templates/Register.js');
var RegisterController = {};
extend(RegisterController, BaseRegisterController);
RegisterController.onRegistered = function(user_id, access_token) {
MatrixClientPeg.replaceUsingAccessToken(
this.state.hs_url, this.state.is_url, user_id, access_token
);
this.setState({
step: 'profile',
busy: true
});
var self = this;
var cli = MatrixClientPeg.get();
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
self.setState({
avatarUrl: result.avatar_url,
busy: false
});
},
function(err) {
console.err(err);
self.setState({
busy: false
});
});
};
RegisterController.onAccountReady = function() {
if (this.props.onLoggedIn) {
this.props.onLoggedIn();
}
};
module.exports = RegisterController;

View File

@ -15,7 +15,18 @@ limitations under the License.
*/
.mx_MemberAvatar {
z-index: 20;
border-radius: 20px;
/* commenting this out as it breaks on FF seemingly */
/* position: relative; */
}
.mx_MemberAvatar_initial {
position: absolute;
color: #fff;
text-align: center;
speak: none;
pointer-events: none;
}
.mx_MemberAvatar_image {
border-radius: 20px;
}

View File

@ -14,22 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
.mx_RoomAvatar {
}
var React = require('react');
var CasLoginController = require('matrix-react-sdk/lib/controllers/organisms/CasLogin');
module.exports = React.createClass({
displayName: 'CasLogin',
mixins: [CasLoginController],
render: function() {
return (
<div>
<button onClick={this.onCasClicked}>Sign in with CAS</button>
</div>
);
},
});
.mx_RoomAvatar_initial {
position: absolute;
color: #fff;
text-align: center;
font-weight: normal ! important;
speak: none;
pointer-events: none;
}

View File

@ -0,0 +1,32 @@
/*
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.
*/
.mx_Spinner {
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
-webkit-justify-content: center;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
flex: 1;
-webkit-flex: 1;
}
.mx_MatrixChat_middlePanel .mx_Spinner {
height: auto;
}

View File

@ -47,6 +47,14 @@ a:visited {
color: #76cfa6;
}
/* XXX: critical hack to GeminiScrollbar to allow them to work in FF 42 and Chrome 48.
Stop the scrollbar view from pushing out the container's overall sizing, which causes
flexbox to adapt to the new size and cause the view to keep growing.
*/
.gm-scrollbar-container .gm-scroll-view {
position: absolute;
}
.mx_ContextualMenu_background {
position: fixed;
top: 0;
@ -91,19 +99,9 @@ a:visited {
margin: 0 auto;
}
.mx_Dialog_background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.2;
z-index: 2000;
}
.mx_Dialog_wrapper {
position: fixed;
z-index: 4000;
top: 0;
left: 0;
width: 100%;
@ -124,7 +122,7 @@ a:visited {
background-color: #fff;
color: #747474;
text-align: center;
z-index: 2010;
z-index: 4010;
font-weight: 300;
font-size: 16px;
position: relative;
@ -132,6 +130,16 @@ a:visited {
max-width: 80%;
}
.mx_Dialog_background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.2;
}
.mx_Dialog_lightbox .mx_Dialog_background {
opacity: 0.85;
}

View File

@ -0,0 +1 @@
../../../../node_modules/react-gemini-scrollbar/node_modules/gemini-scrollbar/gemini-scrollbar.css

View File

@ -0,0 +1 @@
../../../../node_modules/gfm.css/gfm.css

View File

@ -0,0 +1 @@
../../../../node_modules/highlight.js/styles/github.css

View File

@ -1,4 +1,3 @@
.mx_RoomDropTarget,
.mx_RoomSettings_encrypt,
.mx_CreateRoom_encrypt,
.mx_RightPanel_filebutton

View File

@ -18,13 +18,13 @@ limitations under the License.
max-width: 100%;
clear: both;
margin-top: 24px;
margin-left: 56px;
margin-left: 65px;
}
.mx_EventTile_avatar {
padding-left: 18px;
padding-right: 12px;
margin-left: -64px;
margin-left: -73px;
margin-top: -4px;
float: left;
}
@ -49,7 +49,6 @@ limitations under the License.
.mx_EventTile .mx_MessageTimestamp {
color: #acacac;
font-size: 12px;
float: right;
}
.mx_EventTile_line {
@ -66,6 +65,28 @@ limitations under the License.
margin-right: 100px;
}
/* Various markdown overrides */
.mx_MessageTile_content .markdown-body {
font-family: inherit ! important;
white-space: normal ! important;
line-height: inherit ! important;
}
.mx_MessageTile_content .markdown-body h1,
.mx_MessageTile_content .markdown-body h2,
.mx_MessageTile_content .markdown-body h3,
.mx_MessageTile_content .markdown-body h4,
.mx_MessageTile_content .markdown-body h5,
.mx_MessageTile_content .markdown-body h6
{
font-family: inherit ! important;
}
.mx_MessageTile_content .markdown-body a {
color: #76cfa6;
}
.mx_MessageTile_searchHighlight {
background-color: #76cfa6;
color: #fff;
@ -78,7 +99,7 @@ limitations under the License.
}
.mx_EventTile_notSent {
color: #f11;
color: #ddd;
}
.mx_EventTile_highlight {
@ -91,10 +112,18 @@ limitations under the License.
.mx_EventTile_msgOption {
float: right;
text-align: right;
z-index: 1;
position: relative;
width: 90px;
margin-right: 10px;
margin-top: -6px;
}
.mx_MessageTimestamp {
display: block;
visibility: hidden;
text-align: right;
}
.mx_EventTile_last .mx_MessageTimestamp {
@ -107,8 +136,7 @@ limitations under the License.
.mx_EventTile_editButton {
position: absolute;
right: 1px;
top: 15px;
display: inline-block;
visibility: hidden;
}
@ -123,3 +151,21 @@ limitations under the License.
.mx_EventTile.menu .mx_MessageTimestamp {
visibility: visible;
}
.mx_EventTile_readAvatars {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
}
.mx_EventTile_readAvatars .mx_MemberAvatar {
position: absolute;
display: inline-block;
}
.mx_EventTile_readAvatarRemainder {
color: #acacac;
font-size: 12px;
position: absolute;
}

View File

@ -15,5 +15,6 @@ limitations under the License.
*/
.mx_MNoticeTile {
white-space: pre-wrap;
opacity: 0.6;
}

View File

@ -15,20 +15,40 @@ limitations under the License.
*/
.mx_MatrixToolbar {
text-align: center;
background-color: #ff0064;
background-color: #76cfa6;
color: #fff;
font-weight: bold;
padding: 6px;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
}
.mx_MatrixToolbar button {
margin-left: 12px;
.mx_MatrixToolbar_warning {
margin-left: 16px;
margin-right: 8px;
margin-top: -2px;
}
.mx_MatrixToolbar_link
{
color: #fff ! important;
text-decoration: underline ! important;
cursor: pointer;
}
.mx_MatrixToolbar_close {
float: right;
margin-top: 3px;
margin-right: 12px;
-webkit-flex: 1;
flex: 1;
cursor: pointer;
text-align: right;
}
.mx_MatrixToolbar_close img {
display: block;
float: right;
margin-right: 10px;
}

View File

@ -16,29 +16,25 @@ limitations under the License.
.mx_MessageComposer_wrapper {
max-width: 960px;
height: 70px;
vertical-align: middle;
margin: auto;
background-color: #fff;
border-top: 2px solid #e1dddd;
}
.mx_MessageComposer_row {
display: table-row;
width: 100%;
height: 70px;
}
.mx_MessageComposer .mx_MessageComposer_avatar {
display: table-cell;
padding-left: 10px;
padding-right: 20px;
height: 70px;
padding-right: 28px;
vertical-align: middle;
}
.mx_MessageComposer .mx_MessageComposer_avatar img {
margin-top: 18px;
border-radius: 20px;
.mx_MessageComposer .mx_MessageComposer_avatar .mx_MemberAvatar {
display: block;
}
.mx_MessageComposer_input {
@ -49,11 +45,12 @@ limitations under the License.
}
.mx_MessageComposer_input textarea {
display: block;
font-size: 15px;
width: 100%;
height: 1.2em;
padding-top: 0.7em;
padding-bottom: 0.7em;
padding: 0px;
margin-top: 6px;
margin-bottom: 6px;
border: 0px;
resize: none;
outline: none;
@ -75,7 +72,8 @@ limitations under the License.
}
.mx_MessageComposer_upload,
.mx_MessageComposer_call {
.mx_MessageComposer_voicecall,
.mx_MessageComposer_videocall {
display: table-cell;
vertical-align: middle;
padding-left: 10px;
@ -83,7 +81,12 @@ limitations under the License.
cursor: pointer;
}
.mx_MessageComposer_call {
.mx_MessageComposer_videocall {
padding-right: 10px;
padding-top: 4px;
}
.mx_MessageComposer_voicecall {
padding-right: 10px;
padding-top: 4px;
}

View File

@ -16,12 +16,46 @@ limitations under the License.
.mx_RoomDropTarget {
font-size: 14px;
text-align: center;
margin-left: 8px;
margin-right: 8px;
padding-top: 16px;
padding-bottom: 16px;
background-color: #fbfbfb;
border: 1px dashed #d7d7d7;
border-radius: 8px;
margin-left: 10px;
margin-right: 15px;
padding-top: 5px;
padding-bottom: 5px;
border: 1px dashed #76cfa6;
color: #454545;
background-color: rgba(255,255,255,0.5);
border-radius: 4px;
}
.collapsed .mx_RoomDropTarget {
margin-right: 10px;
}
.mx_RoomDropTarget_placeholder {
padding-top: 1px;
padding-bottom: 1px;
}
.mx_RoomDropTarget_avatar {
background-color: #fff;
border-radius: 24px;
width: 24px;
height: 24px;
float: left;
margin-left: 7px;
margin-right: 7px;
}
.mx_RoomDropTarget_label {
position: relative;
margin-top: 3px;
line-height: 21px;
z-index: 1;
}
.collapsed .mx_RoomDropTarget_avatar {
float: none;
}
.collapsed .mx_RoomDropTarget_label {
display: none;
}

View File

@ -33,6 +33,7 @@ limitations under the License.
.mx_RoomHeader_leftRow {
height: 48px;
margin-top: 18px;
margin-left: -2px;
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
@ -89,9 +90,9 @@ limitations under the License.
.mx_RoomHeader_simpleHeader {
line-height: 83px;
color: #76cfa6;
font-weight: 400;
font-size: 20px;
color: #454545;
font-size: 24px;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
@ -101,9 +102,9 @@ limitations under the License.
vertical-align: middle;
height: 28px;
color: #454545;
font-weight: 800;
font-weight: bold;
font-size: 24px;
padding-left: 8px;
padding-left: 19px;
padding-right: 16px;
text-overflow: ellipsis;
}
@ -153,7 +154,7 @@ limitations under the License.
max-height: 38px;
color: #454545;
font-weight: 300;
padding-left: 8px;
padding-left: 19px;
padding-right: 16px;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -16,13 +16,13 @@ limitations under the License.
.mx_RoomTile {
cursor: pointer;
display: table-row;
/* This fixes wrapping of long room names, but breaks drag & drop previews */
/* display: table-row; */
font-size: 14px;
}
.mx_RoomTile_avatar {
display: table-cell;
background: #eaf5f0;
padding-right: 8px;
padding-top: 4px;
padding-bottom: 2px;
@ -39,17 +39,16 @@ limitations under the License.
.mx_RoomTile_name {
display: table-cell;
width: 100%;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 16px;
color: #454545;
opacity: 0.8;
color: rgba(69, 69, 69, 0.8);
}
.mx_RoomTile_invite {
opacity: 0.5;
font-weight: normal;
color: rgba(69, 69, 69, 0.5);
}
.collapsed .mx_RoomTile_name {
@ -105,16 +104,15 @@ limitations under the License.
}
.mx_RoomTile_unread,
.mx_RoomTile_highlight,
.mx_RoomTile_invited
{
.mx_RoomTile_highlight {
font-weight: bold;
}
.mx_RoomTile_selected {
.mx_RoomTile_selected .mx_RoomTile_name {
color: #76cfa6 ! important;
}
.mx_RoomTile.mx_RoomTile_selected {
.mx_RoomTile.mx_RoomTile_selected .mx_RoomTile_name {
background: url('img/selected.png');
background-repeat: no-repeat;
background-position: right center;

View File

@ -21,7 +21,6 @@ limitations under the License.
border-radius: 8px;
background-color: #fff;
z-index: 1000;
margin-top: 6px;
left: 64px;
padding: 6px;
}

View File

@ -34,16 +34,21 @@ limitations under the License.
cursor: pointer;
}
.mx_LeftPanel .mx_RoomList {
.mx_LeftPanel_callView {
}
.mx_LeftPanel .mx_RoomList_scrollbar {
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
-ms-flex-order: 1;
-webkit-order: 1;
order: 1;
overflow-y: auto;
-webkit-flex: 1 1 0;
flex: 1 1 0;
overflow-y: auto;
}
.mx_LeftPanel .mx_BottomLeftMenu {
@ -53,8 +58,10 @@ limitations under the License.
-webkit-order: 3;
order: 3;
-webkit-flex: 0 0 126px;
flex: 0 0 126px;
-webkit-flex: 0 0 140px;
flex: 0 0 140px;
background-color: rgba(118,207,166,0.19);
}
.mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile {
@ -62,7 +69,7 @@ limitations under the License.
}
.mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options {
margin-top: 12px;
margin-top: 17px;
width: 100%;
}

View File

@ -16,13 +16,7 @@ limitations under the License.
.mx_RoomList {
padding-top: 24px;
}
.mx_RoomList_invites,
.mx_RoomList_recents {
display: table;
table-layout: fixed;
width: 100%;
padding-bottom: 12px;
}
.mx_RoomList_expandButton {
@ -32,13 +26,9 @@ limitations under the License.
padding-right: 12px;
}
.mx_RoomList h2 {
text-transform: uppercase;
color: #3d3b39;
font-weight: 600;
font-size: 14px;
padding-left: 12px;
padding-right: 12px;
margin-top: 8px;
margin-bottom: 4px;
/* Evil hacky override until Chrome fixes drop and drag table cells
and we can correctly fix horizontal wrapping in the sidebar again */
.mx_RoomList_scrollbar .gm-scroll-view {
overflow-x: hidden ! important;
overflow-y: scroll ! important;
}

View File

@ -0,0 +1,45 @@
/*
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.
*/
.mx_RoomSubList {
display: table;
table-layout: fixed;
width: 100%;
}
.mx_RoomSubList_bottommost {
/* XXX: this should really be 100% of the RoomList height, but can't seem to get at it */
min-height: 400px;
}
.mx_RoomSubList_label {
text-transform: uppercase;
color: #3d3b39;
font-weight: 600;
font-size: 14px;
padding-left: 12px;
padding-right: 12px;
margin-top: 8px;
margin-bottom: 4px;
}
.mx_RoomSubList_chevron {
padding-left: 5px;
}
.collapsed .mx_RoomSubList_chevron {
padding-left: 13px;
}

View File

@ -125,11 +125,11 @@ limitations under the License.
clear: both;
}
.mx_RoomView_MessageList h2 {
.mx_RoomView_MessageList > h2 {
clear: both;
margin-top: 32px;
margin-bottom: 8px;
margin-left: 54px;
margin-left: 63px;
padding-bottom: 6px;
border-bottom: 1px solid #eee;
}
@ -158,18 +158,19 @@ limitations under the License.
order: 4;
width: 100%;
-webkit-flex: 0 0 36px;
flex: 0 0 36px;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.mx_RoomView_statusAreaBox {
max-width: 960px;
margin: auto;
min-height: 36px;
}
.mx_RoomView_statusAreaBox_line {
border-top: 1px solid #eee;
margin-left: 54px;
margin-left: 63px;
height: 1px;
}
@ -185,16 +186,44 @@ limitations under the License.
vertical-align: middle;
}
.mx_RoomView_connectionLostBar {
margin-top: 19px;
height: 58px;
}
.mx_RoomView_connectionLostBar img {
padding-left: 10px;
padding-right: 22px;
vertical-align: middle;
float: left;
}
.mx_RoomView_connectionLostBar_title {
color: #ff0064;
}
.mx_RoomView_connectionLostBar_desc {
color: #454545;
font-size: 14px;
opacity: 0.5;
}
.mx_RoomView_resend_link {
color: #454545 ! important;
text-decoration: underline ! important;
cursor: pointer;
}
.mx_RoomView_typingBar {
margin-top: 10px;
margin-left: 54px;
margin-left: 63px;
color: #4a4a4a;
opacity: 0.5;
}
.mx_RoomView_typingImage {
display: inline;
margin-left: -38px;
margin-left: -47px;
margin-top: -4px;
float: left;
}
@ -207,14 +236,14 @@ limitations under the License.
order: 5;
width: 100%;
-webkit-flex: 0 0 70px;
flex: 0 0 70px;
-webkit-flex: 0;
flex: 0;
margin-right: 2px;
}
.mx_RoomView_uploadProgressOuter {
height: 4px;
margin-left: 54px;
margin-left: 63px;
margin-top: -1px;
}
@ -225,7 +254,7 @@ limitations under the License.
.mx_RoomView_uploadFilename {
margin-top: 5px;
margin-left: 56px;
margin-left: 65px;
opacity: 0.5;
color: #4a4a4a;
}

View File

@ -14,6 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MatrixChat_splash {
position: relative;
height: 100%;
}
.mx_MatrixChat_splashButtons {
text-align: center;
width: 100%;
position: absolute;
bottom: 30px;
}
.mx_MatrixChat_wrapper {
display: -webkit-box;
display: -moz-box;
@ -35,7 +47,7 @@ limitations under the License.
-webkit-order: 1;
order: 1;
height: 21px;
height: 40px;
}
.mx_MatrixChat_toolbarShowing {
@ -71,8 +83,8 @@ limitations under the License.
background-color: #eaf5f0;
-webkit-flex: 0 0 230px;
flex: 0 0 230px;
-webkit-flex: 0 0 210px;
flex: 0 0 210px;
}
.mx_MatrixChat .mx_LeftPanel.collapsed {
@ -87,8 +99,8 @@ limitations under the License.
-webkit-order: 2;
order: 2;
padding-left: 12px;
padding-right: 12px;
padding-left: 25px;
padding-right: 22px;
background-color: #fff;
-webkit-flex: 1;
@ -97,7 +109,8 @@ limitations under the License.
/* XXX: Hack: apparently if you try to nest a flex-box
* within a non-flex-box within a flex-box, the height
* of the innermost element gets miscalculated if the
* parents are both auto.
* parents are both auto. Height has to be auto here
* for RoomView to correctly fit when the Toolbar is shown.
* Ideally we'd launch straight into the RoomView at this
* point, but instead we fudge it and make the middlePanel
* flex itself.
@ -116,8 +129,8 @@ limitations under the License.
-webkit-order: 3;
order: 3;
-webkit-flex: 0 0 230px;
flex: 0 0 230px;
-webkit-flex: 0 0 235px;
flex: 0 0 235px;
}
.mx_MatrixChat .mx_RightPanel.collapsed {

View File

@ -17,6 +17,18 @@ limitations under the License.
.mx_Login {
width: 100%;
height: 100%;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
overflow: auto;
}
.mx_Login h2 {
@ -28,8 +40,10 @@ limitations under the License.
.mx_Login_box {
width: 300px;
min-height: 450px;
padding-top: 50px;
padding-bottom: 50px;
margin: auto;
padding-top: 100px;
}
.mx_Login_logo {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -65,13 +65,11 @@ skin['molecules.RoomTile'] = require('./views/molecules/RoomTile');
skin['molecules.RoomTooltip'] = require('./views/molecules/RoomTooltip');
skin['molecules.SearchBar'] = require('./views/molecules/SearchBar');
skin['molecules.SenderProfile'] = require('./views/molecules/SenderProfile');
skin['molecules.ServerConfig'] = require('./views/molecules/ServerConfig');
skin['molecules.UnknownMessageTile'] = require('./views/molecules/UnknownMessageTile');
skin['molecules.UserSelector'] = require('./views/molecules/UserSelector');
skin['molecules.voip.CallView'] = require('./views/molecules/voip/CallView');
skin['molecules.voip.IncomingCallBox'] = require('./views/molecules/voip/IncomingCallBox');
skin['molecules.voip.VideoView'] = require('./views/molecules/voip/VideoView');
skin['organisms.CasLogin'] = require('./views/organisms/CasLogin');
skin['organisms.CreateRoom'] = require('./views/organisms/CreateRoom');
skin['organisms.ErrorDialog'] = require('./views/organisms/ErrorDialog');
skin['organisms.LeftPanel'] = require('./views/organisms/LeftPanel');
@ -82,12 +80,11 @@ skin['organisms.QuestionDialog'] = require('./views/organisms/QuestionDialog');
skin['organisms.RightPanel'] = require('./views/organisms/RightPanel');
skin['organisms.RoomDirectory'] = require('./views/organisms/RoomDirectory');
skin['organisms.RoomList'] = require('./views/organisms/RoomList');
skin['organisms.RoomSubList'] = require('./views/organisms/RoomSubList');
skin['organisms.RoomView'] = require('./views/organisms/RoomView');
skin['organisms.UserSettings'] = require('./views/organisms/UserSettings');
skin['organisms.ViewSource'] = require('./views/organisms/ViewSource');
skin['pages.CompatibilityPage'] = require('./views/pages/CompatibilityPage');
skin['pages.MatrixChat'] = require('./views/pages/MatrixChat');
skin['templates.Login'] = require('./views/templates/Login');
skin['templates.Register'] = require('./views/templates/Register');
module.exports = skin;

View File

@ -40,10 +40,32 @@ module.exports = React.createClass({
},
render: function() {
// XXX: recalculates default avatar url constantly
if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
var initial;
if (this.props.member.name[0])
initial = this.props.member.name[0].toUpperCase();
if (initial === '@' && this.props.member.name[1])
initial = this.props.member.name[1].toUpperCase();
return (
<img className="mx_MemberAvatar" src={this.state.imageUrl}
<span className="mx_MemberAvatar" {...this.props}>
<span className="mx_MemberAvatar_initial" aria-hidden="true"
style={{ fontSize: (this.props.width * 0.75) + "px",
width: this.props.width + "px",
lineHeight: this.props.height*1.2 + "px" }}>{ initial }</span>
<img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={this.props.member.name}
onError={this.onError} width={this.props.width} height={this.props.height} />
</span>
);
}
return (
<img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
onError={this.onError}
width={this.props.width} height={this.props.height} />
width={this.props.width} height={this.props.height}
title={this.props.member.name}
{...this.props}
/>
);
}
});

View File

@ -43,13 +43,33 @@ module.exports = React.createClass({
render: function() {
var style = {
maxWidth: this.props.width,
maxHeight: this.props.height,
width: this.props.width,
height: this.props.height,
};
// XXX: recalculates fallback avatar constantly
if (this.state.imageUrl === this.getFallbackAvatar()) {
var initial;
if (this.props.room.name[0])
initial = this.props.room.name[0].toUpperCase();
if ((initial === '@' || initial === '#') && this.props.room.name[1])
initial = this.props.room.name[1].toUpperCase();
return (
<img className="mx_RoomAvatar" src={this.state.imageUrl} onError={this.onError}
style={style}
/>
<span>
<span className="mx_RoomAvatar_initial" aria-hidden="true"
style={{ fontSize: (this.props.width * 0.75) + "px",
width: this.props.width + "px",
lineHeight: this.props.height*1.2 + "px" }}>{ initial }</span>
<img className="mx_RoomAvatar" src={this.state.imageUrl}
onError={this.onError} style={style} />
</span>
);
}
else {
return <img className="mx_RoomAvatar" src={this.state.imageUrl}
onError={this.onError} style={style} />
}
}
});

View File

@ -26,7 +26,7 @@ module.exports = React.createClass({
var h = this.props.h || 32;
var imgClass = this.props.imgClassName || "";
return (
<div>
<div className="mx_Spinner">
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
</div>
);

View File

@ -21,9 +21,6 @@ var React = require('react');
var sdk = require('matrix-react-sdk')
var ChangeAvatarController = require('matrix-react-sdk/lib/controllers/molecules/ChangeAvatar')
var Loader = require("react-loader");
module.exports = React.createClass({
displayName: 'ChangeAvatar',
mixins: [ChangeAvatarController],
@ -70,6 +67,7 @@ module.exports = React.createClass({
</div>
);
case this.Phases.Uploading:
var Loader = sdk.getComponent("atoms.Spinner");
return (
<Loader />
);

View File

@ -20,8 +20,6 @@ var React = require('react');
var sdk = require('matrix-react-sdk');
var ChangeDisplayNameController = require("matrix-react-sdk/lib/controllers/molecules/ChangeDisplayName");
var Loader = require("react-loader");
module.exports = React.createClass({
displayName: 'ChangeDisplayName',
@ -39,6 +37,7 @@ module.exports = React.createClass({
render: function() {
if (this.state.busy) {
var Loader = sdk.getComponent("atoms.Spinner");
return (
<Loader />
);

View File

@ -19,17 +19,15 @@ limitations under the License.
var React = require('react');
var ChangePasswordController = require('matrix-react-sdk/lib/controllers/molecules/ChangePassword')
var Loader = require("react-loader");
module.exports = React.createClass({
displayName: 'ChangePassword',
mixins: [ChangePasswordController],
onClickChange: function() {
var old_password = this.refs.old_input.getDOMNode().value;
var new_password = this.refs.new_input.getDOMNode().value;
var confirm_password = this.refs.confirm_input.getDOMNode().value;
var old_password = this.refs.old_input.value;
var new_password = this.refs.new_input.value;
var confirm_password = this.refs.confirm_input.value;
if (new_password != confirm_password) {
this.setState({
state: this.Phases.Error,
@ -64,6 +62,7 @@ module.exports = React.createClass({
</div>
);
case this.Phases.Uploading:
var Loader = sdk.getComponent("atoms.Spinner");
return (
<div className="mx_Dialog_content">
<Loader />

View File

@ -17,15 +17,28 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDom = require('react-dom');
var classNames = require("classnames");
var sdk = require('matrix-react-sdk')
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg')
var EventTileController = require('matrix-react-sdk/lib/controllers/molecules/EventTile')
var ContextualMenu = require('../../../../ContextualMenu');
var TextForEvent = require('matrix-react-sdk/lib/TextForEvent');
var Velociraptor = require('../../../../Velociraptor');
require('../../../../VelocityBounce');
var bounce = false;
try {
if (global.localStorage) {
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
}
} catch (e) {
}
var eventTileTypes = {
'm.room.message': 'molecules.MessageTile',
'm.room.member' : 'molecules.EventAsTextTile',
@ -36,6 +49,8 @@ var eventTileTypes = {
'm.room.topic' : 'molecules.EventAsTextTile',
};
var MAX_READ_AVATARS = 5;
module.exports = React.createClass({
displayName: 'EventTile',
mixins: [EventTileController],
@ -52,7 +67,7 @@ module.exports = React.createClass({
},
getInitialState: function() {
return {menu: false};
return {menu: false, allReadAvatars: false};
},
onEditClicked: function(e) {
@ -72,6 +87,127 @@ module.exports = React.createClass({
this.setState({menu: true});
},
toggleAllReadAvatars: function() {
this.setState({
allReadAvatars: !this.state.allReadAvatars
});
},
getReadAvatars: function() {
var avatars = [];
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
if (!room) return [];
var myUserId = MatrixClientPeg.get().credentials.userId;
// get list of read receipts, sorted most recent first
var receipts = room.getReceiptsForEvent(this.props.mxEvent).filter(function(r) {
return r.type === "m.read" && r.userId != myUserId;
}).sort(function(r1, r2) {
return r2.data.ts - r1.data.ts;
});
var MemberAvatar = sdk.getComponent('atoms.MemberAvatar');
var left = 0;
var reorderTransitionOpts = {
duration: 100,
easing: 'easeOut'
};
for (var i = 0; i < receipts.length; ++i) {
var member = room.getMember(receipts[i].userId);
// Using react refs here would mean both getting Velociraptor to expose
// them and making them scoped to the whole RoomView. Not impossible, but
// getElementById seems simpler at least for a first cut.
var oldAvatarDomNode = document.getElementById('mx_readAvatar'+member.userId);
var startStyles = [];
var enterTransitionOpts = [];
var oldNodeTop = -15; // For avatars that weren't on screen, act as if they were just off the top
if (oldAvatarDomNode) {
oldNodeTop = oldAvatarDomNode.getBoundingClientRect().top;
}
if (this.readAvatarNode) {
var topOffset = oldNodeTop - this.readAvatarNode.getBoundingClientRect().top;
if (oldAvatarDomNode && oldAvatarDomNode.style.left !== '0px') {
var leftOffset = oldAvatarDomNode.style.left;
// start at the old height and in the old h pos
startStyles.push({ top: topOffset, left: leftOffset });
enterTransitionOpts.push(reorderTransitionOpts);
}
// then shift to the rightmost column,
// and then it will drop down to its resting position
startStyles.push({ top: topOffset, left: '0px' });
enterTransitionOpts.push({
duration: bounce ? Math.min(Math.log(Math.abs(topOffset)) * 200, 3000) : 300,
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
});
}
var style = {
left: left+'px',
top: '0px',
visibility: ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) ? 'visible' : 'hidden'
};
//console.log("i = " + i + ", MAX_READ_AVATARS = " + MAX_READ_AVATARS + ", allReadAvatars = " + this.state.allReadAvatars + " visibility = " + style.visibility);
// add to the start so the most recent is on the end (ie. ends up rightmost)
avatars.unshift(
<MemberAvatar key={member.userId} member={member}
width={14} height={14} resizeMethod="crop"
style={style}
startStyle={startStyles}
enterTransitionOpts={enterTransitionOpts}
id={'mx_readAvatar'+member.userId}
onClick={this.toggleAllReadAvatars}
/>
);
// TODO: we keep the extra read avatars in the dom to make animation simpler
// we could optimise this to reduce the dom size.
if (i < MAX_READ_AVATARS - 1 || this.state.allReadAvatars) { // XXX: where does this -1 come from? is it to make the max'th avatar animate properly?
left -= 15;
}
}
var editButton;
if (!this.state.allReadAvatars) {
var remainder = receipts.length - MAX_READ_AVATARS;
var remText;
if (i >= MAX_READ_AVATARS - 1) left -= 15;
if (remainder > 0) {
remText = <span className="mx_EventTile_readAvatarRemainder"
onClick={this.toggleAllReadAvatars}
style={{ left: left }}>{ remainder }+
</span>;
left -= 15;
}
editButton = (
<input style={{ left: left }}
type="image" src="img/edit.png" alt="Options" title="Options" width="14" height="14"
className="mx_EventTile_editButton" onClick={this.onEditClicked} />
);
}
return <span className="mx_EventTile_readAvatars" ref={this.collectReadAvatarNode}>
{ editButton }
{ remText }
<Velociraptor transition={ reorderTransitionOpts }>
{ avatars }
</Velociraptor>
</span>;
},
collectReadAvatarNode: function(node) {
this.readAvatarNode = ReactDom.findDOMNode(node);
},
render: function() {
var MessageTimestamp = sdk.getComponent('atoms.MessageTimestamp');
var SenderProfile = sdk.getComponent('molecules.SenderProfile');
@ -100,18 +236,14 @@ module.exports = React.createClass({
menu: this.state.menu,
});
var timestamp = <MessageTimestamp ts={this.props.mxEvent.getTs()} />
var editButton = (
<input
type="image" src="img/edit.png" alt="Edit" width="14" height="14"
className="mx_EventTile_editButton" onClick={this.onEditClicked}
/>
);
var aux = null;
if (msgtype === 'm.image') aux = "sent an image";
else if (msgtype === 'm.video') aux = "sent a video";
else if (msgtype === 'm.file') aux = "uploaded a file";
var readAvatars = this.getReadAvatars();
var avatar, sender;
if (!this.props.continuation) {
if (this.props.mxEvent.sender) {
@ -127,11 +259,13 @@ module.exports = React.createClass({
}
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
{ timestamp }
{ readAvatars }
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
{ timestamp }
{ editButton }
<EventTileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} />
</div>
</div>

View File

@ -30,15 +30,25 @@ module.exports = React.createClass({
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
var httpUrl = cli.mxcUrlToHttp(content.url);
var text = this.presentableTextForFile(content);
if (httpUrl) {
return (
<span className="mx_MFileTile">
<div className="mx_MImageTile_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
<img src="img/download.png" width="10" height="12"/>
Download {this.presentableTextForFile(content)}
Download {text}
</a>
</div>
</span>
);
} else {
var extra = text ? ': '+text : '';
return <span className="mx_MFileTile">
Invalid file{extra}
</span>
}
},
});

View File

@ -63,6 +63,34 @@ module.exports = React.createClass({
}
},
_isGif: function() {
var content = this.props.mxEvent.getContent();
return (content && content.info && content.info.mimetype === "image/gif");
},
onImageEnter: function(e) {
if (!this._isGif()) {
return;
}
var imgElement = e.target;
imgElement.src = MatrixClientPeg.get().mxcUrlToHttp(
this.props.mxEvent.getContent().url
);
},
onImageLeave: function(e) {
if (!this._isGif()) {
return;
}
var imgElement = e.target;
imgElement.src = this._getThumbUrl();
},
_getThumbUrl: function() {
var content = this.props.mxEvent.getContent();
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 480, 360);
},
render: function() {
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
@ -73,10 +101,15 @@ module.exports = React.createClass({
var imgStyle = {};
if (thumbHeight) imgStyle['height'] = thumbHeight;
var thumbUrl = this._getThumbUrl();
if (thumbUrl) {
return (
<span className="mx_MImageTile">
<a href={cli.mxcUrlToHttp(content.url)} onClick={ this.onClick }>
<img className="mx_MImageTile_thumbnail" src={cli.mxcUrlToHttp(content.url, 480, 360)} alt={content.body} style={imgStyle} />
<img className="mx_MImageTile_thumbnail" src={thumbUrl}
alt={content.body} style={imgStyle}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
<div className="mx_MImageTile_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
@ -86,5 +119,18 @@ module.exports = React.createClass({
</div>
</span>
);
} else if (content.body) {
return (
<span className="mx_MImageTile">
Image '{content.body}' cannot be displayed.
</span>
);
} else {
return (
<span className="mx_MImageTile">
This image cannot be displayed.
</span>
);
}
},
});

View File

@ -17,67 +17,34 @@ limitations under the License.
'use strict';
var React = require('react');
var sanitizeHtml = require('sanitize-html');
var HtmlUtils = require('../../../../HtmlUtils');
var MNoticeTileController = require('matrix-react-sdk/lib/controllers/molecules/MNoticeTile')
var allowedAttributes = sanitizeHtml.defaults.allowedAttributes;
allowedAttributes['font'] = ['color'];
var sanitizeHtmlParams = {
allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'font' ]),
allowedAttributes: allowedAttributes,
};
module.exports = React.createClass({
displayName: 'MNoticeTile',
mixins: [MNoticeTileController],
// FIXME: this entire class is copy-pasted from MTextTile :(
componentDidMount: function() {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(this.getDOMNode());
},
componentDidUpdate: function() {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(this.getDOMNode());
},
shouldComponentUpdate: function(nextProps) {
// exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.searchTerm !== this.props.searchTerm);
},
// XXX: fix horrible duplication with MTextTile
render: function() {
var content = this.props.mxEvent.getContent();
var originalBody = content.body;
var body;
if (this.props.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(this.props.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 <b/> or whatever will break the HTML badly.
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset, offset) }} />);
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSearchTerm }} className="mx_MessageTile_searchHighlight" />);
lastOffset = offset + safeSearchTerm.length;
}
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset) }} />);
}
else {
while ((offset = originalBody.indexOf(this.props.searchTerm, lastOffset)) >= 0) {
bodyList.push(<span key={ k++ } >{ originalBody.substring(lastOffset, offset) }</span>);
bodyList.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ this.props.searchTerm }</span>);
lastOffset = offset + this.props.searchTerm.length;
}
bodyList.push(<span key={ k++ }>{ originalBody.substring(lastOffset) }</span>);
}
body = bodyList;
}
else {
if (content.format === "org.matrix.custom.html") {
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
body = <span dangerouslySetInnerHTML={{ __html: safeBody }} />;
}
else {
body = originalBody;
}
}
var body = HtmlUtils.bodyToHtml(content, this.props.searchTerm);
return (
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">

View File

@ -17,67 +17,33 @@ limitations under the License.
'use strict';
var React = require('react');
var sanitizeHtml = require('sanitize-html');
var HtmlUtils = require('../../../../HtmlUtils');
var MTextTileController = require('matrix-react-sdk/lib/controllers/molecules/MTextTile')
var allowedAttributes = sanitizeHtml.defaults.allowedAttributes;
allowedAttributes['font'] = ['color'];
var sanitizeHtmlParams = {
allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'font' ]),
allowedAttributes: allowedAttributes,
};
module.exports = React.createClass({
displayName: 'MTextTile',
mixins: [MTextTileController],
// FIXME: this entire class is copy-pasted from MTextTile :(
componentDidMount: function() {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(this.getDOMNode());
},
componentDidUpdate: function() {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(this.getDOMNode());
},
shouldComponentUpdate: function(nextProps) {
// exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.searchTerm !== this.props.searchTerm);
},
render: function() {
var content = this.props.mxEvent.getContent();
var originalBody = content.body;
var body;
if (this.props.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(this.props.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 <b/> or whatever will break the HTML badly.
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset, offset) }} />);
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSearchTerm }} className="mx_MessageTile_searchHighlight" />);
lastOffset = offset + safeSearchTerm.length;
}
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset) }} />);
}
else {
while ((offset = originalBody.indexOf(this.props.searchTerm, lastOffset)) >= 0) {
bodyList.push(<span key={ k++ } >{ originalBody.substring(lastOffset, offset) }</span>);
bodyList.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ this.props.searchTerm }</span>);
lastOffset = offset + this.props.searchTerm.length;
}
bodyList.push(<span key={ k++ }>{ originalBody.substring(lastOffset) }</span>);
}
body = bodyList;
}
else {
if (content.format === "org.matrix.custom.html") {
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
body = <span dangerouslySetInnerHTML={{ __html: safeBody }} />;
}
else {
body = originalBody;
}
}
var body = HtmlUtils.bodyToHtml(content, this.props.searchTerm);
return (
<span ref="content" className="mx_MTextTile mx_MessageTile_content">

View File

@ -28,12 +28,19 @@ module.exports = React.createClass({
Notifier.setToolbarHidden(true);
},
onClick: function() {
var Notifier = sdk.getComponent('organisms.Notifier');
Notifier.setEnabled(true);
},
render: function() {
var EnableNotificationsButton = sdk.getComponent("atoms.EnableNotificationsButton");
return (
<div className="mx_MatrixToolbar">
You are not receiving desktop notifications. <EnableNotificationsButton />
<div className="mx_MatrixToolbar_close"><img src="img/close-white.png" width="16" height="16" onClick={ this.hideToolbar } /></div>
<img className="mx_MatrixToolbar_warning" src="img/warning.png" width="28" height="28" alt="/!\"/>
<div>
You are not receiving desktop notifications. <a className="mx_MatrixToolbar_link" onClick={ this.onClick }>Enable them now</a>
</div>
<div className="mx_MatrixToolbar_close"><img src="img/cancel-black2.png" width="23" height="23" onClick={ this.hideToolbar } /></div>
</div>
);
}

View File

@ -17,7 +17,6 @@ limitations under the License.
'use strict';
var React = require('react');
var Loader = require("../atoms/Spinner");
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var sdk = require('matrix-react-sdk')
@ -47,6 +46,7 @@ module.exports = React.createClass({
}
if (this.state.creatingRoom) {
var Loader = sdk.getComponent("atoms.Spinner");
spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>;
}

View File

@ -28,8 +28,12 @@ module.exports = React.createClass({
displayName: 'MessageComposer',
mixins: [MessageComposerController],
onInputClick: function(ev) {
this.refs.textarea.focus();
},
onUploadClick: function(ev) {
this.refs.uploadInput.getDOMNode().click();
this.refs.uploadInput.click();
},
onUploadFileSelected: function(ev) {
@ -38,7 +42,7 @@ module.exports = React.createClass({
if (files && files.length > 0) {
this.props.uploadFile(files[0]);
}
this.refs.uploadInput.getDOMNode().value = null;
this.refs.uploadInput.value = null;
},
onCallClick: function(ev) {
@ -49,6 +53,14 @@ module.exports = React.createClass({
});
},
onVoiceCallClick: function(ev) {
dis.dispatch({
action: 'place_call',
type: 'voice',
room_id: this.props.room.roomId
});
},
render: function() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'};
@ -60,15 +72,18 @@ module.exports = React.createClass({
<div className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} />
</div>
<div className="mx_MessageComposer_input">
<textarea ref="textarea" onKeyDown={this.onKeyDown} placeholder="Type a message..." />
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
<textarea ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
</div>
<div className="mx_MessageComposer_upload" onClick={this.onUploadClick}>
<img src="img/upload.png" width="17" height="22"/>
<img src="img/upload.png" alt="Upload file" title="Upload file" width="17" height="22"/>
<input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
</div>
<div className="mx_MessageComposer_call" onClick={this.onCallClick}>
<img src="img/call.png" width="28" height="20"/>
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick}>
<img src="img/voice.png" alt="Voice call" title="Voice call" width="16" height="26"/>
</div>
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick}>
<img src="img/call.png" alt="Video call" title="Video call" width="28" height="20"/>
</div>
</div>
</div>

View File

@ -22,25 +22,13 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var dis = require('matrix-react-sdk/lib/dispatcher');
var sdk = require('matrix-react-sdk')
var Modal = require('matrix-react-sdk/lib/Modal');
var Resend = require("../../../../Resend");
module.exports = React.createClass({
displayName: 'MessageContextMenu',
onResendClick: function() {
MatrixClientPeg.get().resendEvent(
this.props.mxEvent, MatrixClientPeg.get().getRoom(
this.props.mxEvent.getRoomId()
)
).done(function() {
dis.dispatch({
action: 'message_sent'
});
}, function() {
dis.dispatch({
action: 'message_send_failed'
});
});
dis.dispatch({action: 'message_resend_started'});
Resend.resend(this.props.mxEvent);
if (this.props.onFinished) this.props.onFinished();
},

View File

@ -18,16 +18,25 @@ limitations under the License.
var React = require('react');
//var RoomDropTargetController = require('matrix-react-sdk/lib/controllers/molecules/RoomDropTargetController')
module.exports = React.createClass({
displayName: 'RoomDropTarget',
// mixins: [RoomDropTargetController],
render: function() {
if (this.props.placeholder) {
return (
<div className="mx_RoomDropTarget">
{this.props.text}
<div className="mx_RoomDropTarget mx_RoomDropTarget_placeholder">
</div>
);
}
else {
return (
<div className="mx_RoomDropTarget">
<div className="mx_RoomDropTarget_avatar"></div>
<div className="mx_RoomDropTarget_label">
{ this.props.label }
</div>
</div>
);
}
}
});

View File

@ -35,7 +35,7 @@ module.exports = React.createClass({
},
getRoomName: function() {
return this.refs.name_edit.getDOMNode().value;
return this.refs.name_edit.value;
},
onFullscreenClick: function() {

View File

@ -27,15 +27,15 @@ module.exports = React.createClass({
mixins: [RoomSettingsController],
getTopic: function() {
return this.refs.topic.getDOMNode().value;
return this.refs.topic.value;
},
getJoinRules: function() {
return this.refs.is_private.getDOMNode().checked ? "invite" : "public";
return this.refs.is_private.checked ? "invite" : "public";
},
getHistoryVisibility: function() {
return this.refs.share_history.getDOMNode().checked ? "shared" : "invited";
return this.refs.share_history.checked ? "shared" : "invited";
},
getPowerLevels: function() {
@ -45,13 +45,13 @@ module.exports = React.createClass({
power_levels = power_levels.getContent();
var new_power_levels = {
ban: parseInt(this.refs.ban.getDOMNode().value),
kick: parseInt(this.refs.kick.getDOMNode().value),
redact: parseInt(this.refs.redact.getDOMNode().value),
invite: parseInt(this.refs.invite.getDOMNode().value),
events_default: parseInt(this.refs.events_default.getDOMNode().value),
state_default: parseInt(this.refs.state_default.getDOMNode().value),
users_default: parseInt(this.refs.users_default.getDOMNode().value),
ban: parseInt(this.refs.ban.value),
kick: parseInt(this.refs.kick.value),
redact: parseInt(this.refs.redact.value),
invite: parseInt(this.refs.invite.value),
events_default: parseInt(this.refs.events_default.value),
state_default: parseInt(this.refs.state_default.value),
users_default: parseInt(this.refs.users_default.value),
users: power_levels.users,
events: power_levels.events,
};

View File

@ -17,6 +17,8 @@ limitations under the License.
'use strict';
var React = require('react');
var DragSource = require('react-dnd').DragSource;
var DropTarget = require('react-dnd').DropTarget;
var classNames = require('classnames');
var RoomTileController = require('matrix-react-sdk/lib/controllers/molecules/RoomTile')
@ -25,10 +27,179 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var sdk = require('matrix-react-sdk')
module.exports = React.createClass({
/**
* Specifies the drag source contract.
* Only `beginDrag` function is required.
*/
var roomTileSource = {
canDrag: function(props, monitor) {
return props.roomSubList.props.editable;
},
beginDrag: function (props) {
// Return the data describing the dragged item
var item = {
room: props.room,
originalList: props.roomSubList,
originalIndex: props.roomSubList.findRoomTile(props.room).index,
targetList: props.roomSubList, // at first target is same as original
// lastTargetRoom: null,
// lastYOffset: null,
// lastYDelta: null,
};
if (props.roomSubList.debug) console.log("roomTile beginDrag for " + item.room.roomId);
// doing this 'correctly' with state causes react-dnd to break seemingly due to the state transitions
props.room._dragging = true;
return item;
},
endDrag: function (props, monitor, component) {
var item = monitor.getItem();
if (props.roomSubList.debug) console.log("roomTile endDrag for " + item.room.roomId + " with didDrop=" + monitor.didDrop());
props.room._dragging = false;
if (monitor.didDrop()) {
if (props.roomSubList.debug) console.log("force updating component " + item.targetList.props.label);
item.targetList.forceUpdate(); // as we're not using state
}
if (monitor.didDrop() && item.targetList.props.editable) {
// if we moved lists, remove the old tag
if (item.targetList !== item.originalList) {
// commented out attempts to set a spinner on our target component as component is actually
// the original source component being dragged, not our target. To fix we just need to
// move all of this to endDrop in the target instead. FIXME later.
//component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 });
MatrixClientPeg.get().deleteRoomTag(item.room.roomId, item.originalList.props.tagName).finally(function() {
//component.state.set({ spinner: component.state.spinner-- });
}).fail(function(err) {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to remove tag " + item.originalList.props.tagName + " from room",
description: err.toString()
});
});
}
var newOrder= {};
if (item.targetList.props.order === 'manual') {
newOrder['order'] = item.targetList.calcManualOrderTagData(item.room);
}
// if we moved lists or the ordering changed, add the new tag
if (item.targetList.props.tagName && (item.targetList !== item.originalList || newOrder)) {
//component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 });
MatrixClientPeg.get().setRoomTag(item.room.roomId, item.targetList.props.tagName, newOrder).finally(function() {
//component.state.set({ spinner: component.state.spinner-- });
}).fail(function(err) {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to add tag " + item.targetList.props.tagName + " to room",
description: err.toString()
});
});
}
}
else {
// cancel the drop and reset our original position
if (props.roomSubList.debug) console.log("cancelling drop & drag");
props.roomSubList.moveRoomTile(item.room, item.originalIndex);
if (item.targetList && item.targetList !== item.originalList) {
item.targetList.removeRoomTile(item.room);
}
}
}
};
var roomTileTarget = {
canDrop: function() {
return false;
},
hover: function(props, monitor) {
var item = monitor.getItem();
//var off = monitor.getClientOffset();
// console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver());
//console.log("item.targetList=" + item.targetList + ", roomSubList=" + props.roomSubList);
var switchedTarget = false;
if (item.targetList !== props.roomSubList) {
// we've switched target, so remove the tile from the previous target.
// n.b. the previous target might actually be the source list.
if (props.roomSubList.debug) console.log("switched target sublist");
switchedTarget = true;
item.targetList.removeRoomTile(item.room);
item.targetList = props.roomSubList;
}
if (!item.targetList.props.editable) return;
if (item.targetList.props.order === 'manual') {
if (item.room.roomId !== props.room.roomId && props.room !== item.lastTargetRoom) {
// find the offset of the target tile in the list.
var roomTile = props.roomSubList.findRoomTile(props.room);
// shuffle the list to add our tile to that position.
props.roomSubList.moveRoomTile(item.room, roomTile.index);
}
// stop us from flickering between our droptarget and the previous room.
// whenever the cursor changes direction we have to reset the flicker-damping.
/*
var yDelta = off.y - item.lastYOffset;
if ((yDelta > 0 && item.lastYDelta < 0) ||
(yDelta < 0 && item.lastYDelta > 0))
{
// the cursor changed direction - forget our previous room
item.lastTargetRoom = null;
}
else {
// track the last room we were hovering over so we can stop
// bouncing back and forth if the droptarget is narrower than
// the other list items. The other way to do this would be
// to reduce the size of the hittarget on the list items, but
// can't see an easy way to do that.
item.lastTargetRoom = props.room;
}
if (yDelta) item.lastYDelta = yDelta;
item.lastYOffset = off.y;
*/
}
else if (switchedTarget) {
if (!props.roomSubList.findRoomTile(item.room).room) {
// add to the list in the right place
props.roomSubList.moveRoomTile(item.room, 0);
}
// we have to sort the list whatever to recalculate it
props.roomSubList.sortList();
}
},
};
var RoomTile = React.createClass({
displayName: 'RoomTile',
mixins: [RoomTileController],
propTypes: {
connectDragSource: React.PropTypes.func.isRequired,
connectDropTarget: React.PropTypes.func.isRequired,
isDragging: React.PropTypes.bool.isRequired,
room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired,
selected: React.PropTypes.bool.isRequired,
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired,
roomSubList: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return( { hover : false });
},
@ -42,21 +213,34 @@ module.exports = React.createClass({
},
render: function() {
// if (this.props.clientOffset) {
// //console.log("room " + this.props.room.roomId + " has dropTarget clientOffset " + this.props.clientOffset.x + "," + this.props.clientOffset.y);
// }
/*
if (this.props.room._dragging) {
var RoomDropTarget = sdk.getComponent("molecules.RoomDropTarget");
return <RoomDropTarget placeholder={true}/>;
}
*/
var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId];
var classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_highlight': this.props.highlight,
'mx_RoomTile_invited': this.props.room.currentState.members[myUserId].membership == 'invite'
'mx_RoomTile_invited': (me && me.membership == 'invite'),
});
var name;
if (this.props.isInvite) {
name = this.props.room.getMember(MatrixClientPeg.get().credentials.userId).events.member.getSender();
name = this.props.room.getMember(myUserId).events.member.getSender();
}
else {
name = this.props.room.name;
// XXX: We should never display raw room IDs, but sometimes the room name js sdk gives is undefined
name = this.props.room.name || this.props.room.roomId;
}
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
@ -91,7 +275,14 @@ module.exports = React.createClass({
}
var RoomAvatar = sdk.getComponent('atoms.RoomAvatar');
return (
// These props are injected by React DnD,
// as defined by your `collect` function above:
var isDragging = this.props.isDragging;
var connectDragSource = this.props.connectDragSource;
var connectDropTarget = this.props.connectDropTarget;
return connectDragSource(connectDropTarget(
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className="mx_RoomTile_avatar">
<RoomAvatar room={this.props.room} width="24" height="24" />
@ -99,6 +290,27 @@ module.exports = React.createClass({
</div>
{ label }
</div>
);
));
}
});
// Export the wrapped version, inlining the 'collect' functions
// to more closely resemble the ES7
module.exports =
DropTarget('RoomTile', roomTileTarget, function(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
}
})(
DragSource('RoomTile', roomTileSource, function(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource(),
// You can ask the monitor about the current drag state:
isDragging: monitor.isDragging()
};
})(RoomTile));

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
var dis = require('matrix-react-sdk/lib/dispatcher');
@ -24,21 +25,21 @@ module.exports = React.createClass({
displayName: 'RoomTooltip',
componentDidMount: function() {
var tooltip = ReactDOM.findDOMNode(this);
if (!this.props.bottom) {
// tell the roomlist about us so it can position us
dis.dispatch({
action: 'view_tooltip',
tooltip: this.getDOMNode(),
tooltip: tooltip,
});
}
else {
var tooltip = this.getDOMNode();
tooltip.style.top = tooltip.parentElement.getBoundingClientRect().top + "px";
tooltip.style.display = "block";
}
},
componentDidUnmount: function() {
componentWillUnmount: function() {
if (!this.props.bottom) {
dis.dispatch({
action: 'view_tooltip',

View File

@ -39,7 +39,7 @@ module.exports = React.createClass({
onSearchChange: function(e) {
if (e.keyCode === 13) { // on enter...
this.props.onSearch(this.refs.search_term.getDOMNode().value, this.state.scope);
this.props.onSearch(this.refs.search_term.value, this.state.scope);
}
},

View File

@ -1,50 +0,0 @@
/*
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 Modal = require('matrix-react-sdk/lib/Modal');
var sdk = require('matrix-react-sdk')
var ServerConfigController = require('matrix-react-sdk/lib/controllers/molecules/ServerConfig')
module.exports = React.createClass({
displayName: 'ServerConfig',
mixins: [ServerConfigController],
showHelpPopup: function() {
var ErrorDialog = sdk.getComponent('organisms.ErrorDialog');
Modal.createDialog(ErrorDialog, {
title: 'Custom Server Options',
description: "You can use the custom server options to log into other Matrix servers by specifying a different Home server URL. This allows you to use Vector with an existing Matrix account on a different Home server. You can also set a cutom Identity server but this will affect people ability to find you if you use a server in a group other than tha main Matrix.org group.",
button: "Dismiss",
focus: true
});
},
render: function() {
return (
<div className="mx_ServerConfig">
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">Home server URL</label>
<input className="mx_Login_field" id="hsurl" type="text" value={this.state.hs_url} onChange={this.hsChanged} />
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">Identity server URL</label>
<input className="mx_Login_field" type="text" value={this.state.is_url} onChange={this.isChanged} />
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>What does this mean?</a>
</div>
);
}
});

View File

@ -25,8 +25,8 @@ module.exports = React.createClass({
mixins: [UserSelectorController],
onAddUserId: function() {
this.addUser(this.refs.user_id_input.getDOMNode().value);
this.refs.user_id_input.getDOMNode().value = "";
this.addUser(this.refs.user_id_input.value);
this.refs.user_id_input.value = "";
},
render: function() {

View File

@ -34,7 +34,7 @@ module.exports = React.createClass({
render: function(){
var VideoView = sdk.getComponent('molecules.voip.VideoView');
return (
<VideoView ref="video"/>
<VideoView ref="video" onClick={ this.props.onClick }/>
);
}
});

View File

@ -27,7 +27,7 @@ module.exports = React.createClass({
mixins: [IncomingCallBoxController],
getRingAudio: function() {
return this.refs.ringAudio.getDOMNode();
return this.refs.ringAudio;
},
render: function() {

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher')
@ -29,15 +30,15 @@ module.exports = React.createClass({
},
getRemoteVideoElement: function() {
return this.refs.remote.getDOMNode();
return ReactDOM.findDOMNode(this.refs.remote);
},
getRemoteAudioElement: function() {
return this.refs.remoteAudio.getDOMNode();
return this.refs.remoteAudio;
},
getLocalVideoElement: function() {
return this.refs.local.getDOMNode();
return ReactDOM.findDOMNode(this.refs.local);
},
setContainer: function(c) {
@ -50,7 +51,7 @@ module.exports = React.createClass({
if (!this.container) {
return;
}
var element = this.container.getDOMNode();
var element = this.container;
if (payload.fullscreen) {
var requestMethod = (
element.requestFullScreen ||
@ -78,7 +79,7 @@ module.exports = React.createClass({
render: function() {
var VideoFeed = sdk.getComponent('atoms.voip.VideoFeed');
return (
<div className="mx_VideoView" ref={this.setContainer}>
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
<div className="mx_VideoView_remoteVideoFeed">
<VideoFeed ref="remote"/>
<audio ref="remoteAudio"/>

View File

@ -24,9 +24,6 @@ var sdk = require('matrix-react-sdk')
var PresetValues = require('matrix-react-sdk/lib/controllers/atoms/create_room/Presets').Presets;
var Loader = require("react-loader");
module.exports = React.createClass({
displayName: 'CreateRoom',
mixins: [CreateRoomController],
@ -122,6 +119,7 @@ module.exports = React.createClass({
render: function() {
var curr_phase = this.state.phase;
if (curr_phase == this.phases.CREATING) {
var Loader = sdk.getComponent("atoms.Spinner");
return (
<Loader/>
);

View File

@ -17,18 +17,72 @@ limitations under the License.
'use strict';
var React = require('react');
var DragDropContext = require('react-dnd').DragDropContext;
var HTML5Backend = require('react-dnd-html5-backend');
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
module.exports = React.createClass({
var CallHandler = require("matrix-react-sdk/lib/CallHandler");
var LeftPanel = React.createClass({
displayName: 'LeftPanel',
getInitialState: function() {
return {
showCallElement: null,
};
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
componentWillReceiveProps: function(newProps) {
this._recheckCallElement(newProps.selectedRoom);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
switch (payload.action) {
// listen for call state changes to prod the render method, which
// may hide the global CallView if the call it is tracking is dead
case 'call_state':
this._recheckCallElement(this.props.selectedRoom);
break;
}
},
_recheckCallElement: function(selectedRoomId) {
// if we aren't viewing a room with an ongoing call, but there is an
// active call, show the call element - we need to do this to make
// audio/video not crap out
var activeCall = CallHandler.getAnyActiveCall();
var callForRoom = CallHandler.getCallForRoom(selectedRoomId);
var showCall = (activeCall && !callForRoom);
this.setState({
showCallElement: showCall
});
},
onHideClick: function() {
dis.dispatch({
action: 'hide_left_panel',
});
},
onCallViewClick: function() {
var call = CallHandler.getAnyActiveCall();
if (call) {
dis.dispatch({
action: 'view_room',
room_id: call.roomId,
});
}
},
render: function() {
var RoomList = sdk.getComponent('organisms.RoomList');
var BottomLeftMenu = sdk.getComponent('molecules.BottomLeftMenu');
@ -44,10 +98,17 @@ module.exports = React.createClass({
// collapseButton = <img className="mx_LeftPanel_hideButton" onClick={ this.onHideClick } src="img/hide.png" width="12" height="20" alt="<"/>
}
var callPreview;
if (this.state.showCallElement) {
var CallView = sdk.getComponent('molecules.voip.CallView');
callPreview = <CallView className="mx_LeftPanel_callView" onClick={this.onCallViewClick} />
}
return (
<aside className={classes}>
{ collapseButton }
<IncomingCallBox />
{ callPreview }
<RoomList selectedRoom={this.props.selectedRoom} collapsed={this.props.collapsed}/>
<BottomLeftMenu collapsed={this.props.collapsed}/>
</aside>
@ -55,3 +116,4 @@ module.exports = React.createClass({
}
});
module.exports = DragDropContext(HTML5Backend)(LeftPanel);

View File

@ -18,9 +18,9 @@ limitations under the License.
var React = require('react');
var classNames = require('classnames');
var Loader = require('react-loader');
var MemberListController = require('matrix-react-sdk/lib/controllers/organisms/MemberList')
var GeminiScrollbar = require('react-gemini-scrollbar');
var sdk = require('matrix-react-sdk')
@ -71,12 +71,13 @@ module.exports = React.createClass({
},
onPopulateInvite: function(e) {
this.onInvite(this.refs.invite.getDOMNode().value);
this.onInvite(this.refs.invite.value);
e.preventDefault();
},
inviteTile: function() {
if (this.state.inviting) {
var Loader = sdk.getComponent("atoms.Spinner");
return (
<Loader />
);
@ -104,7 +105,7 @@ module.exports = React.createClass({
}
return (
<div className="mx_MemberList">
<div className="mx_MemberList_border">
<GeminiScrollbar autoshow={true} className="mx_MemberList_border">
{this.inviteTile()}
<div>
<div className="mx_MemberList_wrapper">
@ -112,7 +113,7 @@ module.exports = React.createClass({
</div>
</div>
{invitedSection}
</div>
</GeminiScrollbar>
</div>
);
}

View File

@ -23,8 +23,6 @@ var Modal = require('matrix-react-sdk/lib/Modal');
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
var Loader = require("react-loader");
module.exports = React.createClass({
displayName: 'RoomDirectory',
@ -110,9 +108,9 @@ module.exports = React.createClass({
onKeyUp: function(ev) {
this.forceUpdate();
this.setState({ roomAlias : this.refs.roomAlias.getDOMNode().value })
this.setState({ roomAlias : this.refs.roomAlias.value })
if (ev.key == "Enter") {
this.joinRoom(this.refs.roomAlias.getDOMNode().value);
this.joinRoom(this.refs.roomAlias.value);
}
if (ev.key == "Down") {
@ -121,6 +119,7 @@ module.exports = React.createClass({
render: function() {
if (this.state.loading) {
var Loader = sdk.getComponent("atoms.Spinner");
return (
<div className="mx_RoomDirectory">
<Loader />
@ -136,7 +135,9 @@ module.exports = React.createClass({
<input ref="roomAlias" placeholder="Join a room (e.g. #foo:domain.com)" className="mx_RoomDirectory_input" size="64" onKeyUp={ this.onKeyUp }/>
<div className="mx_RoomDirectory_tableWrapper">
<table className="mx_RoomDirectory_table">
<thead>
<tr><th width="45%">Room</th><th width="45%">Alias</th><th width="10%">Members</th></tr>
</thead>
{ this.getRows(this.state.roomAlias) }
</table>
</div>

View File

@ -20,6 +20,7 @@ var React = require('react');
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
var GeminiScrollbar = require('react-gemini-scrollbar');
var RoomListController = require('../../../../controllers/organisms/RoomList')
module.exports = React.createClass({
@ -33,48 +34,82 @@ module.exports = React.createClass({
},
render: function() {
var CallView = sdk.getComponent('molecules.voip.CallView');
var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget');
var callElement;
if (this.state.show_call_element) {
callElement = <CallView className="mx_MatrixChat_callView"/>
}
var expandButton = this.props.collapsed ?
<img className="mx_RoomList_expandButton" onClick={ this.onShowClick } src="img/menu.png" width="20" alt=">"/> :
null;
var invitesLabel = this.props.collapsed ? null : "Invites";
var recentsLabel = this.props.collapsed ? null : "Recent";
var invites;
if (this.state.inviteList.length) {
invites = <div>
<h2 className="mx_RoomList_invitesLabel">{ invitesLabel }</h2>
<div className="mx_RoomList_invites">
{this.makeRoomTiles(this.state.inviteList, true)}
</div>
</div>
}
var RoomSubList = sdk.getComponent('organisms.RoomSubList');
var self = this;
return (
<div className="mx_RoomList" onScroll={this._repositionTooltip}>
<GeminiScrollbar className="mx_RoomList_scrollbar" autoshow={true} onScroll={self._repositionTooltip}>
<div className="mx_RoomList">
{ expandButton }
{ callElement }
<h2 className="mx_RoomList_favouritesLabel">Favourites</h2>
<RoomDropTarget text="Drop here to favourite"/>
{ invites }
<RoomSubList list={ self.state.lists['m.invite'] }
label="Invites"
editable={ false }
order="recent"
activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed } />
<h2 className="mx_RoomList_recentsLabel">{ recentsLabel }</h2>
<div className="mx_RoomList_recents">
{this.makeRoomTiles(this.state.roomList, false)}
</div>
<h2 className="mx_RoomList_archiveLabel">Archive</h2>
<RoomDropTarget text="Drop here to archive"/>
<RoomSubList list={ self.state.lists['m.favourite'] }
label="Favourites"
tagName="m.favourite"
verb="favourite"
editable={ true }
order="manual"
activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed } />
<RoomSubList list={ self.state.lists['m.recent'] }
label="Conversations"
editable={ true }
verb="restore"
order="recent"
activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed } />
{ Object.keys(self.state.lists).map(function(tagName) {
if (!tagName.match(/^m\.(invite|favourite|recent|lowpriority|archived)$/)) {
return <RoomSubList list={ self.state.lists[tagName] }
key={ tagName }
label={ tagName }
tagName={ tagName }
verb={ "tag as " + tagName }
editable={ true }
order="manual"
activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed } />
}
}) }
<RoomSubList list={ self.state.lists['m.lowpriority'] }
label="Low priority"
tagName="m.lowpriority"
verb="demote"
editable={ true }
order="recent"
bottommost={ self.state.lists['m.archived'].length === 0 }
activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed } />
<RoomSubList list={ self.state.lists['m.archived'] }
label="Historical"
editable={ false }
order="recent"
bottommost={ true }
activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed } />
</div>
</GeminiScrollbar>
);
}
});

View File

@ -0,0 +1,290 @@
/*
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 DropTarget = require('react-dnd').DropTarget;
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
// turn this on for drop & drag console debugging galore
var debug = false;
var roomListTarget = {
canDrop: function() {
return true;
},
drop: function(props, monitor, component) {
if (debug) console.log("dropped on sublist")
},
hover: function(props, monitor, component) {
var item = monitor.getItem();
if (component.state.sortedList.length == 0 && props.editable) {
if (debug) console.log("hovering on sublist " + props.label + ", isOver=" + monitor.isOver());
if (item.targetList !== component) {
item.targetList.removeRoomTile(item.room);
item.targetList = component;
}
component.moveRoomTile(item.room, 0);
}
},
};
var RoomSubList = React.createClass({
displayName: 'RoomSubList',
debug: debug,
propTypes: {
list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
label: React.PropTypes.string.isRequired,
tagName: React.PropTypes.string,
editable: React.PropTypes.bool,
order: React.PropTypes.string.isRequired,
bottommost: React.PropTypes.bool,
selectedRoom: React.PropTypes.string.isRequired,
activityMap: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return {
hidden: false,
sortedList: [],
};
},
componentWillMount: function() {
this.sortList(this.props.list, this.props.order);
},
componentWillReceiveProps: function(newProps) {
// order the room list appropriately before we re-render
//if (debug) console.log("received new props, list = " + newProps.list);
this.sortList(newProps.list, newProps.order);
},
onClick: function(ev) {
this.setState({ hidden : !this.state.hidden });
},
tsOfNewestEvent: function(room) {
if (room.timeline.length) {
return room.timeline[room.timeline.length - 1].getTs();
}
else {
return Number.MAX_SAFE_INTEGER;
}
},
// TODO: factor the comparators back out into a generic comparator
// so that view_prev_room and view_next_room can do the right thing
recentsComparator: function(roomA, roomB) {
return this.tsOfNewestEvent(roomB) - this.tsOfNewestEvent(roomA);
},
manualComparator: function(roomA, roomB) {
if (!roomA.tags[this.props.tagName] || !roomB.tags[this.props.tagName]) return 0;
var a = roomA.tags[this.props.tagName].order;
var b = roomB.tags[this.props.tagName].order;
return a == b ? this.recentsComparator(roomA, roomB) : ( a > b ? 1 : -1);
},
sortList: function(list, order) {
if (list === undefined) list = this.state.sortedList;
if (order === undefined) order = this.props.order;
var comparator;
list = list || [];
if (order === "manual") comparator = this.manualComparator;
if (order === "recent") comparator = this.recentsComparator;
//if (debug) console.log("sorting list for sublist " + this.props.label + " with length " + list.length + ", this.props.list = " + this.props.list);
this.setState({ sortedList: list.sort(comparator) });
},
moveRoomTile: function(room, atIndex) {
if (debug) console.log("moveRoomTile: id " + room.roomId + ", atIndex " + atIndex);
//console.log("moveRoomTile before: " + JSON.stringify(this.state.rooms));
var found = this.findRoomTile(room);
var rooms = this.state.sortedList;
if (found.room) {
if (debug) console.log("removing at index " + found.index + " and adding at index " + atIndex);
rooms.splice(found.index, 1);
rooms.splice(atIndex, 0, found.room);
}
else {
if (debug) console.log("Adding at index " + atIndex);
rooms.splice(atIndex, 0, room);
}
this.setState({ sortedList: rooms });
// console.log("moveRoomTile after: " + JSON.stringify(this.state.rooms));
},
// XXX: this isn't invoked via a property method but indirectly via
// the roomList property method. Unsure how evil this is.
removeRoomTile: function(room) {
if (debug) console.log("remove room " + room.roomId);
var found = this.findRoomTile(room);
var rooms = this.state.sortedList;
if (found.room) {
rooms.splice(found.index, 1);
}
else {
console.warn("Can't remove room " + room.roomId + " - can't find it");
}
this.setState({ sortedList: rooms });
},
findRoomTile: function(room) {
var index = this.state.sortedList.indexOf(room);
if (index >= 0) {
// console.log("found: room: " + room.roomId + " with index " + index);
}
else {
if (debug) console.log("didn't find room");
room = null;
}
return ({
room: room,
index: index,
});
},
calcManualOrderTagData: function(room) {
var index = this.state.sortedList.indexOf(room);
// we sort rooms by the lexicographic ordering of the 'order' metadata on their tags.
// for convenience, we calculate this for now a floating point number between 0.0 and 1.0.
var orderA = 0.0; // by default we're next to the beginning of the list
if (index > 0) {
var prevTag = this.state.sortedList[index - 1].tags[this.props.tagName];
if (!prevTag) {
console.error("Previous room in sublist is not tagged to be in this list. This should never happen.")
}
else if (prevTag.order === undefined) {
console.error("Previous room in sublist has no ordering metadata. This should never happen.");
}
else {
orderA = prevTag.order;
}
}
var orderB = 1.0; // by default we're next to the end of the list too
if (index < this.state.sortedList.length - 1) {
var nextTag = this.state.sortedList[index + 1].tags[this.props.tagName];
if (!nextTag) {
console.error("Next room in sublist is not tagged to be in this list. This should never happen.")
}
else if (nextTag.order === undefined) {
console.error("Next room in sublist has no ordering metadata. This should never happen.");
}
else {
orderB = nextTag.order;
}
}
var order = (orderA + orderB) / 2.0;
if (order === orderA || order === orderB) {
console.error("Cannot describe new list position. This should be incredibly unlikely.");
// TODO: renumber the list
}
return order;
},
makeRoomTiles: function() {
var self = this;
var RoomTile = sdk.getComponent("molecules.RoomTile");
return this.state.sortedList.map(function(room) {
var selected = room.roomId == self.props.selectedRoom;
// XXX: is it evil to pass in self as a prop to RoomTile?
return (
<RoomTile
room={ room }
roomSubList={ self }
key={ room.roomId }
collapsed={ self.props.collapsed || false}
selected={ selected }
unread={ self.props.activityMap[room.roomId] === 1 }
highlight={ self.props.activityMap[room.roomId] === 2 }
isInvite={ self.props.label === 'Invites' } />
);
});
},
render: function() {
var connectDropTarget = this.props.connectDropTarget;
var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget');
var label = this.props.collapsed ? null : this.props.label;
//console.log("render: " + JSON.stringify(this.state.sortedList));
var target;
if (this.state.sortedList.length == 0 && this.props.editable) {
target = <RoomDropTarget label={ 'Drop here to ' + this.props.verb }/>;
}
if (this.state.sortedList.length > 0 || this.props.editable) {
var subList;
var classes = "mx_RoomSubList" +
(this.props.bottommost ? " mx_RoomSubList_bottommost" : "");
if (!this.state.hidden) {
subList = <div className={ classes }>
{ target }
{ this.makeRoomTiles() }
</div>;
}
else {
subList = <div className={ classes }>
</div>;
}
return connectDropTarget(
<div>
<h2 onClick={ this.onClick } className="mx_RoomSubList_label">{ this.props.collapsed ? '' : this.props.label }
<img className="mx_RoomSubList_chevron" src={ this.state.hidden ? "img/list-open.png" : "img/list-close.png" } width="10" height="10"/>
</h2>
{ subList }
</div>
);
}
else {
return (
<div className="mx_RoomSubList">
</div>
);
}
}
});
// Export the wrapped version, inlining the 'collect' functions
// to more closely resemble the ES7
module.exports =
DropTarget('RoomTile', roomListTarget, function(connect) {
return {
connectDropTarget: connect.dropTarget(),
}
})(RoomSubList);

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var dis = require('matrix-react-sdk/lib/dispatcher');
@ -25,11 +26,9 @@ var sdk = require('matrix-react-sdk')
var classNames = require("classnames");
var filesize = require('filesize');
var GeminiScrollbar = require('react-gemini-scrollbar');
var RoomViewController = require('../../../../controllers/organisms/RoomView')
var Loader = require("react-loader");
module.exports = React.createClass({
displayName: 'RoomView',
mixins: [RoomViewController],
@ -102,9 +101,9 @@ module.exports = React.createClass({
},
scrollToBottom: function() {
if (!this.refs.messageWrapper) return;
var messageWrapper = this.refs.messageWrapper.getDOMNode();
messageWrapper.scrollTop = messageWrapper.scrollHeight;
var scrollNode = this._getScrollNode();
if (!scrollNode) return;
scrollNode.scrollTop = scrollNode.scrollHeight;
},
render: function() {
@ -131,6 +130,7 @@ module.exports = React.createClass({
var myUserId = MatrixClientPeg.get().credentials.userId;
if (this.state.room.currentState.members[myUserId].membership == 'invite') {
if (this.state.joining || this.state.rejecting) {
var Loader = sdk.getComponent("atoms.Spinner");
return (
<div className="mx_RoomView">
<Loader />
@ -196,10 +196,48 @@ module.exports = React.createClass({
);
} else {
var typingString = this.getWhoIsTypingString();
//typingString = "Testing typing...";
var unreadMsgs = this.getUnreadMessagesString();
// no conn bar trumps unread count since you can't get unread messages
// without a connection! (technically may already have some but meh)
// It also trumps the "some not sent" msg since you can't resend without
// a connection!
if (this.state.syncState === "ERROR") {
statusBar = (
<div className="mx_RoomView_connectionLostBar">
<img src="img/warning2.png" width="30" height="30" alt="/!\"/>
<div className="mx_RoomView_connectionLostBar_textArea">
<div className="mx_RoomView_connectionLostBar_title">
Connectivity to the server has been lost.
</div>
<div className="mx_RoomView_connectionLostBar_desc">
Sent messages will be stored until your connection has returned.
</div>
</div>
</div>
);
}
else if (this.state.hasUnsentMessages) {
statusBar = (
<div className="mx_RoomView_connectionLostBar">
<img src="img/warning2.png" width="30" height="30" alt="/!\"/>
<div className="mx_RoomView_connectionLostBar_textArea">
<div className="mx_RoomView_connectionLostBar_title">
Some of your messages have not been sent.
</div>
<div className="mx_RoomView_connectionLostBar_desc">
<a className="mx_RoomView_resend_link"
onClick={ this.onResendAllClick }>
Resend all now
</a> or select individual messages to re-send.
</div>
</div>
</div>
);
}
// unread count trumps who is typing since the unread count is only
// set when you've scrolled up
if (unreadMsgs) {
else if (unreadMsgs) {
statusBar = (
<div className="mx_RoomView_unreadMessagesBar" onClick={ this.scrollToBottom }>
<img src="img/newmessages.png" width="24" height="24" alt=""/>
@ -222,6 +260,7 @@ module.exports = React.createClass({
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} room={this.state.room} />;
}
else if (this.state.uploadingRoomSettings) {
var Loader = sdk.getComponent("atoms.Spinner");
aux = <Loader/>;
}
else if (this.state.searching) {
@ -260,7 +299,7 @@ module.exports = React.createClass({
{ conferenceCallNotification }
{ aux }
</div>
<div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }>
<GeminiScrollbar autoshow={true} ref="messagePanel" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }>
<div className="mx_RoomView_messageListWrapper">
{ fileDropTarget }
<ol className="mx_RoomView_MessageList" aria-live="polite">
@ -269,14 +308,14 @@ module.exports = React.createClass({
{this.getEventTiles()}
</ol>
</div>
</div>
</GeminiScrollbar>
<div className="mx_RoomView_statusArea">
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{statusBar}
</div>
</div>
<MessageComposer room={this.state.room} uploadFile={this.uploadFile} />
<MessageComposer room={this.state.room} roomView={this} uploadFile={this.uploadFile} />
</div>
);
}

View File

@ -19,8 +19,6 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var UserSettingsController = require('matrix-react-sdk/lib/controllers/organisms/UserSettings')
var Loader = require("react-loader");
var Modal = require('matrix-react-sdk/lib/Modal');
module.exports = React.createClass({
@ -68,6 +66,7 @@ module.exports = React.createClass({
},
render: function() {
var Loader = sdk.getComponent("atoms.Spinner");
switch (this.state.phase) {
case this.Phases.Loading:
return <Loader />

View File

@ -21,12 +21,14 @@ var sdk = require('matrix-react-sdk')
var MatrixChatController = require('matrix-react-sdk/lib/controllers/pages/MatrixChat')
// should be atomised
var Loader = require("react-loader");
var dis = require('matrix-react-sdk/lib/dispatcher');
var Matrix = require("matrix-js-sdk");
var ContextualMenu = require("../../../../ContextualMenu");
var Login = require("../../../../components/login/Login");
var Registration = require("../../../../components/login/Registration");
var PostRegistration = require("../../../../components/login/PostRegistration");
var config = require("../../../../../config.json");
module.exports = React.createClass({
displayName: 'MatrixChat',
@ -63,6 +65,14 @@ module.exports = React.createClass({
});
},
onLogoutClick: function(event) {
dis.dispatch({
action: 'logout'
});
event.stopPropagation();
event.preventDefault();
},
handleResize: function(e) {
var hideLhsThreshold = 1000;
var showLhsThreshold = 1000;
@ -92,19 +102,46 @@ module.exports = React.createClass({
});
},
onRegisterClick: function() {
this.showScreen("register");
},
onLoginClick: function() {
this.showScreen("login");
},
onRegistered: function(credentials) {
this.onLoggedIn(credentials);
// do post-registration stuff
this.showScreen("post_registration");
},
onFinishPostRegistration: function() {
// Don't confuse this with "PageType" which is the middle window to show
this.setState({
screen: undefined
});
this.showScreen("settings");
},
render: function() {
var LeftPanel = sdk.getComponent('organisms.LeftPanel');
var RoomView = sdk.getComponent('organisms.RoomView');
var RightPanel = sdk.getComponent('organisms.RightPanel');
var Login = sdk.getComponent('templates.Login');
var UserSettings = sdk.getComponent('organisms.UserSettings');
var Register = sdk.getComponent('templates.Register');
var CreateRoom = sdk.getComponent('organisms.CreateRoom');
var RoomDirectory = sdk.getComponent('organisms.RoomDirectory');
var MatrixToolbar = sdk.getComponent('molecules.MatrixToolbar');
var Notifier = sdk.getComponent('organisms.Notifier');
if (this.state.logged_in && this.state.ready) {
// needs to be before normal PageTypes as you are logged in technically
if (this.state.screen == 'post_registration') {
return (
<PostRegistration
onComplete={this.onFinishPostRegistration} />
);
}
else if (this.state.logged_in && this.state.ready) {
var page_element;
var right_panel = "";
@ -154,20 +191,32 @@ module.exports = React.createClass({
);
}
} else if (this.state.logged_in) {
var Spinner = sdk.getComponent('atoms.Spinner');
return (
<Loader />
<div className="mx_MatrixChat_splash">
<Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>Logout</a>
</div>
);
} else if (this.state.screen == 'register') {
return (
<Register onLoggedIn={this.onLoggedIn} clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id} idSid={this.state.register_id_sid}
hsUrl={this.state.register_hs_url} isUrl={this.state.register_is_url}
<Registration
clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
hsUrl={config.default_hs_url}
isUrl={config.default_is_url}
registrationUrl={this.props.registrationUrl}
/>
onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick} />
);
} else {
return (
<Login onLoggedIn={this.onLoggedIn} />
<Login
onLoggedIn={this.onLoggedIn}
onRegisterClick={this.onRegisterClick}
homeserverUrl={config.default_hs_url}
identityServerUrl={config.default_is_url} />
);
}
}

View File

@ -1,194 +0,0 @@
/*
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 sdk = require('matrix-react-sdk')
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var Loader = require("react-loader");
var LoginController = require('matrix-react-sdk/lib/controllers/templates/Login')
var config = require('../../../../../config.json');
module.exports = React.createClass({
displayName: 'Login',
mixins: [LoginController],
getInitialState: function() {
return {
serverConfigVisible: false
};
},
componentWillMount: function() {
this.onHSChosen();
this.customHsUrl = config.default_hs_url;
this.customIsUrl = config.default_is_url;
},
getHsUrl: function() {
if (this.state.serverConfigVisible) {
return this.customHsUrl;
} else {
return config.default_hs_url;
}
},
getIsUrl: function() {
if (this.state.serverConfigVisible) {
return this.customIsUrl;
} else {
return config.default_is_url;
}
},
onServerConfigVisibleChange: function(ev) {
this.setState({
serverConfigVisible: ev.target.checked
}, this.onHsUrlChanged);
},
/**
* Gets the form field values for the current login stage
*/
getFormVals: function() {
return {
'username': this.refs.user.getDOMNode().value.trim(),
'password': this.refs.pass.getDOMNode().value.trim()
};
},
onHsUrlChanged: function() {
var newHsUrl = this.refs.serverConfig.getHsUrl().trim();
var newIsUrl = this.refs.serverConfig.getIsUrl().trim();
if (newHsUrl == this.customHsUrl &&
newIsUrl == this.customIsUrl)
{
return;
}
else {
this.customHsUrl = newHsUrl;
this.customIsUrl = newIsUrl;
}
MatrixClientPeg.replaceUsingUrls(
this.getHsUrl(),
this.getIsUrl()
);
this.setState({
hs_url: this.getHsUrl(),
is_url: this.getIsUrl()
});
// XXX: HSes do not have to offer password auth, so we
// need to update and maybe show a different component
// when a new HS is entered.
if (this.updateHsTimeout) {
clearTimeout(this.updateHsTimeout);
}
var self = this;
this.updateHsTimeout = setTimeout(function() {
self.onHSChosen();
}, 1000);
},
componentForStep: function(step) {
switch (step) {
case 'choose_hs':
case 'fetch_stages':
var serverConfigStyle = {};
serverConfigStyle.display = this.state.serverConfigVisible ? 'block' : 'none';
var ServerConfig = sdk.getComponent("molecules.ServerConfig");
return (
<div>
<input className="mx_Login_checkbox" id="advanced" type="checkbox" checked={this.state.serverConfigVisible} onChange={this.onServerConfigVisibleChange} />
<label className="mx_Login_label" htmlFor="advanced">Use custom server options (advanced)</label>
<div style={serverConfigStyle}>
<ServerConfig ref="serverConfig"
defaultHsUrl={this.customHsUrl} defaultIsUrl={this.customIsUrl}
onHsUrlChanged={this.onHsUrlChanged}
/>
</div>
</div>
);
// XXX: clearly these should be separate organisms
case 'stage_m.login.password':
return (
<div>
<form onSubmit={this.onUserPassEntered}>
<input className="mx_Login_field" ref="user" type="text" value={this.state.username} onChange={this.onUsernameChanged} placeholder="Email or user name" /><br />
<input className="mx_Login_field" ref="pass" type="password" value={this.state.password} onChange={this.onPasswordChanged} placeholder="Password" /><br />
{ this.componentForStep('choose_hs') }
<input className="mx_Login_submit" type="submit" value="Log in" />
</form>
</div>
);
case 'stage_m.login.cas':
var CasLogin = sdk.getComponent('organisms.CasLogin');
return (
<CasLogin />
);
}
},
onUsernameChanged: function(ev) {
this.setState({username: ev.target.value});
},
onPasswordChanged: function(ev) {
this.setState({password: ev.target.value});
},
loginContent: function() {
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
return (
<div>
<h2>Sign in</h2>
{this.componentForStep(this.state.step)}
<div className="mx_Login_error">
{ loader }
{this.state.errorText}
</div>
<a className="mx_Login_create" onClick={this.showRegister} href="#">Create a new account</a>
<br/>
<div className="mx_Login_links">
<a href="https://medium.com/@Vector">blog</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
<a href="https://twitter.com/@VectorCo">twitter</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
<a href="https://github.com/vector-im/vector-web">github</a>&nbsp;&nbsp;&middot;&nbsp;&nbsp;
<a href="https://matrix.org">powered by Matrix</a>
</div>
</div>
);
},
render: function() {
return (
<div className="mx_Login">
<div className="mx_Login_box">
<div className="mx_Login_logo">
<img src="img/logo.png" width="249" height="78" alt="vector"/>
</div>
{this.loginContent()}
</div>
</div>
);
}
});

View File

@ -1,201 +0,0 @@
/*
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 sdk = require('matrix-react-sdk')
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg')
var Loader = require("react-loader");
var RegisterController = require('../../../../controllers/templates/Register')
var config = require('../../../../../config.json');
module.exports = React.createClass({
displayName: 'Register',
mixins: [RegisterController],
getInitialState: function() {
return {
serverConfigVisible: false
};
},
componentWillMount: function() {
this.customHsUrl = config.default_hs_url;
this.customIsUrl = config.default_is_url;
},
getRegFormVals: function() {
return {
email: this.refs.email.getDOMNode().value.trim(),
username: this.refs.username.getDOMNode().value.trim(),
password: this.refs.password.getDOMNode().value.trim(),
confirmPassword: this.refs.confirmPassword.getDOMNode().value.trim()
};
},
getHsUrl: function() {
if (this.state.serverConfigVisible) {
return this.customHsUrl;
} else {
return config.default_hs_url;
}
},
getIsUrl: function() {
if (this.state.serverConfigVisible) {
return this.customIsUrl;
} else {
return config.default_is_url;
}
},
onServerConfigVisibleChange: function(ev) {
this.setState({
serverConfigVisible: ev.target.checked
});
},
onServerUrlChanged: function(newUrl) {
this.customHsUrl = this.refs.serverConfig.getHsUrl();
this.customIsUrl = this.refs.serverConfig.getIsUrl();
this.forceUpdate();
},
onProfileContinueClicked: function() {
this.onAccountReady();
},
componentForStep: function(step) {
switch (step) {
case 'initial':
var serverConfigStyle = {};
serverConfigStyle.display = this.state.serverConfigVisible ? 'block' : 'none';
var ServerConfig = sdk.getComponent("molecules.ServerConfig");
return (
<div>
<form onSubmit={this.onInitialStageSubmit}>
<input className="mx_Login_field" type="text" ref="email" placeholder="Email address" defaultValue={this.savedParams.email} /><br />
<input className="mx_Login_field" type="text" ref="username" placeholder="User name" defaultValue={this.savedParams.username} /><br />
<input className="mx_Login_field" type="password" ref="password" placeholder="Password" defaultValue={this.savedParams.password} /><br />
<input className="mx_Login_field" type="password" ref="confirmPassword" placeholder="Confirm password" defaultValue={this.savedParams.confirmPassword} /><br />
<input className="mx_Login_checkbox" id="advanced" type="checkbox" value={this.state.serverConfigVisible} onChange={this.onServerConfigVisibleChange} />
<label htmlFor="advanced">Use custom server options (advanced)</label>
<div style={serverConfigStyle}>
<ServerConfig ref="serverConfig"
defaultHsUrl={this.customHsUrl} defaultIsUrl={this.customIsUrl}
onHsUrlChanged={this.onServerUrlChanged} onIsUrlChanged={this.onServerUrlChanged} />
</div>
<br />
<input className="mx_Login_submit" type="submit" value="Register" />
</form>
</div>
);
// XXX: clearly these should be separate organisms
case 'stage_m.login.email.identity':
return (
<div>
Please check your email to continue registration.
</div>
);
case 'stage_m.login.recaptcha':
return (
<div ref="recaptchaContainer">
This Home Server would like to make sure you are not a robot
<div id="mx_recaptcha"></div>
</div>
);
}
},
registerContent: function() {
if (this.state.busy) {
return (
<Loader />
);
} else if (this.state.step == 'profile') {
var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName');
var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar');
return (
<div className="mx_Login_profile">
Set a display name:
<ChangeDisplayName />
Upload an avatar:
<ChangeAvatar initialAvatarUrl={MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl)} />
<button onClick={this.onProfileContinueClicked}>Continue</button>
</div>
);
} else {
return (
<div>
<h2>Create an account</h2>
{this.componentForStep(this.state.step)}
<div className="mx_Login_error">{this.state.errorText}</div>
<a className="mx_Login_create" onClick={this.showLogin} href="#">I already have an account</a>
</div>
);
}
},
onBadFields: function(bad) {
var keys = Object.keys(bad);
var strings = [];
for (var i = 0; i < keys.length; ++i) {
switch (bad[keys[i]]) {
case this.FieldErrors.PasswordMismatch:
strings.push("Passwords don't match");
break;
case this.FieldErrors.Missing:
strings.push("Missing "+keys[i]);
break;
case this.FieldErrors.TooShort:
strings.push(keys[i]+" is too short");
break;
case this.FieldErrors.InUse:
strings.push(keys[i]+" is already taken");
break;
case this.FieldErrors.Length:
strings.push(keys[i] + " is not long enough.");
break;
default:
console.error("Unhandled FieldError: %s", bad[keys[i]]);
break;
}
}
var errtxt = strings.join(', ');
this.setState({
errorText: errtxt
});
},
render: function() {
return (
<div className="mx_Login">
<div className="mx_Login_box">
<div className="mx_Login_logo">
<img src="img/logo.png" width="249" height="78" alt="vector"/>
</div>
{this.registerContent()}
</div>
</div>
);
}
});

View File

@ -18,6 +18,7 @@ limitations under the License.
var RunModernizrTests = require("./modernizr"); // this side-effects a global
var React = require("react");
var ReactDOM = require("react-dom");
var sdk = require("matrix-react-sdk");
sdk.loadSkin(require('../skins/vector/skindex'));
sdk.loadModule(require('../modules/VectorConferenceHandler'));
@ -65,14 +66,21 @@ function parseQsFromFragment(location) {
return {};
}
function parseQs(location) {
return qs.parse(location.search.substring(1));
}
// Here, we do some crude URL analysis to allow
// deep-linking. We only support registration
// deep-links in this example.
function routeUrl(location) {
if (location.hash.indexOf('#/register') == 0) {
var params = parseQs(location);
var loginToken = params.loginToken;
if (loginToken) {
window.matrixChat.showScreen('token_login', parseQs(location));
}
else if (location.hash.indexOf('#/register') == 0) {
window.matrixChat.showScreen('register', parseQsFromFragment(location));
} else if (location.hash.indexOf('#/login/cas') == 0) {
window.matrixChat.showScreen('cas_login', parseQsFromFragment(location));
} else {
window.matrixChat.showScreen(location.hash.substring(2));
}
@ -129,7 +137,7 @@ window.onload = function() {
function loadApp() {
if (validBrowser) {
var MatrixChat = sdk.getComponent('pages.MatrixChat');
window.matrixChat = React.render(
window.matrixChat = ReactDOM.render(
<MatrixChat onNewScreen={onNewScreen} registrationUrl={makeRegistrationUrl()} />,
document.getElementById('matrixchat')
);
@ -138,7 +146,7 @@ function loadApp() {
console.error("Browser is missing required features.");
// take to a different landing page to AWOOOOOGA at the user
var CompatibilityPage = require("../skins/vector/views/pages/CompatibilityPage");
window.matrixChat = React.render(
window.matrixChat = ReactDOM.render(
<CompatibilityPage onAccept={function() {
validBrowser = true;
console.log("User accepts the compatibility risks.");