Merge pull request #39 from matrix-org/kegan/controller-merging3

Phase 3 controller merging
This commit is contained in:
Kegsay 2015-11-27 16:17:31 +00:00
commit 144feb6af9
34 changed files with 2404 additions and 413 deletions

View file

@ -23,13 +23,16 @@
"filesize": "^3.1.2",
"flux": "^2.0.3",
"glob": "^5.0.14",
"highlight.js": "^8.9.1",
"linkifyjs": "^2.0.0-beta.4",
"marked": "^0.3.5",
"matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop",
"optimist": "^0.6.1",
"q": "^1.4.1",
"react": "^0.14.2",
"react-dom": "^0.14.2"
"react-dom": "^0.14.2",
"sanitize-html": "^1.11.1",
"velocity-animate": "^1.2.3"
},
"//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder",
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472",

82
src/ContextualMenu.js Normal file
View file

@ -0,0 +1,82 @@
/*
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');
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container",
getOrCreateContainer: function() {
var container = document.getElementById(this.ContextualMenuContainerId);
if (!container) {
container = document.createElement("div");
container.id = this.ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
},
createMenu: function (Element, props) {
var self = this;
var closeMenu = function() {
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) props.onFinished.apply(null, arguments);
};
var position = {
top: props.top - 20,
};
var chevron = null;
if (props.left) {
chevron = <img className="mx_ContextualMenu_chevron_left" src="img/chevron-left.png" width="9" height="16" />
position.left = props.left + 8;
} else {
chevron = <img className="mx_ContextualMenu_chevron_right" src="img/chevron-right.png" width="9" height="16" />
position.right = props.right + 8;
}
var className = 'mx_ContextualMenu_wrapper';
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click!
var menu = (
<div className={className}>
<div className="mx_ContextualMenu" style={position}>
{chevron}
<Element {...props} onFinished={closeMenu}/>
</div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
</div>
);
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]);
}
},
}

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

@ -14,16 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require('react');
module.exports = {
module.exports = React.createClass({
displayName: 'ProgressBar',
propTypes: {
room: React.PropTypes.object.isRequired,
value: React.PropTypes.number,
max: React.PropTypes.number
},
getInitialState: function() {
return {
power_levels_changed: false
render: function() {
// Would use an HTML5 progress tag but if that doesn't animate if you
// use the HTML attributes rather than styles
var progressStyle = {
width: ((this.props.value / this.props.max) * 100)+"%"
};
return (
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
);
}
};
});

View file

@ -18,7 +18,9 @@ limitations under the License.
var React = require('react');
module.exports = {
module.exports = React.createClass({
displayName: 'UserSelector',
propTypes: {
onChange: React.PropTypes.func,
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
@ -42,4 +44,26 @@ module.exports = {
return e != user_id;
}));
},
};
onAddUserId: function() {
this.addUser(this.refs.user_id_input.value);
this.refs.user_id_input.value = "";
},
render: function() {
var self = this;
return (
<div>
<ul className="mx_UserSelector_UserIdList" ref="list">
{this.props.selected_users.map(function(user_id, i) {
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>
})}
</ul>
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/>
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
Add User
</button>
</div>
);
}
});

View file

@ -0,0 +1,276 @@
/*
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 classNames = require("classnames");
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg')
var TextForEvent = require('../../../TextForEvent');
var ContextualMenu = require('../../../ContextualMenu');
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': 'messages.Message',
'm.room.member' : 'messages.TextualEvent',
'm.call.invite' : 'messages.TextualEvent',
'm.call.answer' : 'messages.TextualEvent',
'm.call.hangup' : 'messages.TextualEvent',
'm.room.name' : 'messages.TextualEvent',
'm.room.topic' : 'messages.TextualEvent',
};
var MAX_READ_AVATARS = 5;
module.exports = React.createClass({
displayName: 'Event',
statics: {
haveTileForEvent: function(e) {
if (eventTileTypes[e.getType()] == undefined) return false;
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== '';
} else {
return true;
}
}
},
getInitialState: function() {
return {menu: false, allReadAvatars: false};
},
shouldHighlight: function() {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
return actions.tweaks.highlight;
},
onEditClicked: function(e) {
var MessageContextMenu = sdk.getComponent('molecules.MessageContextMenu');
var buttonRect = e.target.getBoundingClientRect()
var x = buttonRect.right;
var y = buttonRect.top + (e.target.height / 2);
var self = this;
ContextualMenu.createMenu(MessageContextMenu, {
mxEvent: this.props.mxEvent,
left: x,
top: y,
onFinished: function() {
self.setState({menu: false});
}
});
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('avatars.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('messages.MessageTimestamp');
var SenderProfile = sdk.getComponent('molecules.SenderProfile');
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var content = this.props.mxEvent.getContent();
var msgtype = content.msgtype;
var EventTileType = sdk.getComponent(eventTileTypes[this.props.mxEvent.getType()]);
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!EventTileType) {
throw new Error("Event type not supported");
}
var classes = classNames({
mx_EventTile: true,
mx_EventTile_sending: ['sending', 'queued'].indexOf(
this.props.mxEvent.status
) !== -1,
mx_EventTile_notSent: this.props.mxEvent.status == 'not_sent',
mx_EventTile_highlight: this.shouldHighlight(),
mx_EventTile_continuation: this.props.continuation,
mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual,
menu: this.state.menu,
});
var timestamp = <MessageTimestamp ts={this.props.mxEvent.getTs()} />
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) {
avatar = (
<div className="mx_EventTile_avatar">
<MemberAvatar member={this.props.mxEvent.sender} width={24} height={24} />
</div>
);
}
if (EventTileType.needsSenderProfile()) {
sender = <SenderProfile mxEvent={this.props.mxEvent} aux={aux} />;
}
}
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
{ timestamp }
{ readAvatars }
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<EventTileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} />
</div>
</div>
);
},
});

View file

@ -16,15 +16,29 @@ limitations under the License.
'use strict';
var React = require('react');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../linkify-matrix');
var linkifyMatrix = require('../../../linkify-matrix');
linkifyMatrix(linkify);
module.exports = {
module.exports = React.createClass({
displayName: 'MEmoteMessage',
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
}
};
},
render: function() {
var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent();
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
return (
<span ref="content" className="mx_MEmoteTile mx_MessageTile_content">
* {name} {content.body}
</span>
);
},
});

View file

@ -16,9 +16,13 @@ limitations under the License.
'use strict';
var React = require('react');
var filesize = require('filesize');
var MatrixClientPeg = require('../../../MatrixClientPeg');
module.exports = React.createClass({
displayName: 'MFileMessage',
module.exports = {
presentableTextForFile: function(content) {
var linkText = 'Attachment';
if (content.body && content.body.length > 0) {
@ -39,6 +43,31 @@ module.exports = {
linkText += ' (' + additionals.join(', ') + ')';
}
return linkText;
}
};
},
render: function() {
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 {text}
</a>
</div>
</span>
);
} else {
var extra = text ? ': '+text : '';
return <span className="mx_MFileTile">
Invalid file{extra}
</span>
}
},
});

View file

@ -0,0 +1,136 @@
/*
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 filesize = require('filesize');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'MImageMessage',
thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
// log this because it's spammy
return undefined;
}
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
// no scaling needs to be applied
return fullHeight;
}
var widthMulti = thumbWidth / fullWidth;
var heightMulti = thumbHeight / fullHeight;
if (widthMulti < heightMulti) {
// width is the dominant dimension so scaling will be fixed on that
return Math.floor(widthMulti * fullHeight);
}
else {
// height is the dominant dimension so scaling will be fixed on that
return Math.floor(heightMulti * fullHeight);
}
},
onClick: function(ev) {
if (ev.button == 0 && !ev.metaKey) {
ev.preventDefault();
var content = this.props.mxEvent.getContent();
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(content.url);
var ImageView = sdk.getComponent("elements.ImageView");
Modal.createDialog(ImageView, {
src: httpUrl,
width: content.info.w,
height: content.info.h,
mxEvent: this.props.mxEvent,
}, "mx_Dialog_lightbox");
}
},
_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();
var thumbHeight = null;
if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 480, 360);
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={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">
<img src="img/download.png" width="10" height="12"/>
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
</a>
</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

@ -0,0 +1,59 @@
/*
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 linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix.js');
linkifyMatrix(linkify);
var HtmlUtils = require('../../../HtmlUtils');
module.exports = React.createClass({
displayName: 'MNoticeMessage',
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
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 body = HtmlUtils.bodyToHtml(content, this.props.searchTerm);
return (
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
{ body }
</span>
);
},
});

View file

@ -0,0 +1,52 @@
/*
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('../../../index');
var TextForEvent = require('../../../TextForEvent');
module.exports = React.createClass({
displayName: 'MRoomMemberEvent',
getMemberEventText: function() {
return TextForEvent.textForEvent(this.props.mxEvent);
},
render: function() {
// XXX: for now, just cheekily borrow the css from message tile...
var timestamp = this.props.last ? <MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
var text = this.getMemberEventText();
if (!text) return <div/>;
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
return (
<div className="mx_MessageTile mx_MessageTile_notice">
<div className="mx_MessageTile_avatar">
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
{ timestamp }
<span className="mx_SenderProfile"></span>
<span className="mx_MessageTile_content">
{ text }
</span>
</div>
);
},
});

View file

@ -0,0 +1,59 @@
/*
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 HtmlUtils = require('../../../HtmlUtils');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
linkifyMatrix(linkify);
module.exports = React.createClass({
displayName: 'MTextMessage',
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
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 body = HtmlUtils.bodyToHtml(content, this.props.searchTerm);
return (
<span ref="content" className="mx_MTextTile mx_MessageTile_content">
{ body }
</span>
);
},
});

View file

@ -0,0 +1,83 @@
/*
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 filesize = require('filesize');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'MVideoMessage',
thumbScale: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
// log this because it's spammy
return undefined;
}
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
// no scaling needs to be applied
return fullHeight;
}
var widthMulti = thumbWidth / fullWidth;
var heightMulti = thumbHeight / fullHeight;
if (widthMulti < heightMulti) {
// width is the dominant dimension so scaling will be fixed on that
return widthMulti;
}
else {
// height is the dominant dimension so scaling will be fixed on that
return heightMulti;
}
},
render: function() {
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
var height = null;
var width = null;
var poster = null;
var preload = "metadata";
if (content.info) {
var scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
if (scale) {
width = Math.floor(content.info.w * scale);
height = Math.floor(content.info.h * scale);
}
if (content.info.thumbnail_url) {
poster = cli.mxcUrlToHttp(content.info.thumbnail_url);
preload = "none";
}
}
return (
<span className="mx_MVideoTile">
<video className="mx_MVideoTile" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
controls preload={preload} autoplay="false" loop
height={height} width={width} poster={poster}>
</video>
</span>
);
},
});

View file

@ -0,0 +1,52 @@
/*
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('../../../index');
module.exports = React.createClass({
displayName: 'Message',
statics: {
needsSenderProfile: function() {
return true;
}
},
render: function() {
var UnknownMessageTile = sdk.getComponent('messages.UnknownMessage');
var tileTypes = {
'm.text': sdk.getComponent('messages.MTextMessage'),
'm.notice': sdk.getComponent('messages.MNoticeMessage'),
'm.emote': sdk.getComponent('messages.MEmoteMessage'),
'm.image': sdk.getComponent('messages.MImageMessage'),
'm.file': sdk.getComponent('messages.MFileMessage'),
'm.video': sdk.getComponent('messages.MVideoMessage')
};
var content = this.props.mxEvent.getContent();
var msgtype = content.msgtype;
var TileType = UnknownMessageTile;
if (msgtype && tileTypes[msgtype]) {
TileType = tileTypes[msgtype];
}
return <TileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} />;
},
});

View file

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
var marked = require("marked");
marked.setOptions({
renderer: new marked.Renderer(),
@ -25,12 +25,12 @@ marked.setOptions({
smartLists: true,
smartypants: false
});
var MatrixClientPeg = require("../../MatrixClientPeg");
var SlashCommands = require("../../SlashCommands");
var Modal = require("../../Modal");
var sdk = require('../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
var dis = require("../../dispatcher");
var dis = require("../../../dispatcher");
var KeyCode = {
ENTER: 13,
BACKSPACE: 8,
@ -58,10 +58,11 @@ function mdownToHtml(mdown) {
return html;
}
module.exports = {
oldScrollHeight: 0,
module.exports = React.createClass({
displayName: 'MessageComposer',
componentWillMount: function() {
this.oldScrollHeight = 0;
this.markdownEnabled = MARKDOWN_ENABLED;
this.tabStruct = {
completing: false,
@ -501,7 +502,69 @@ module.exports = {
clearTimeout(this.typingTimeout);
this.typingTimeout = null;
}
},
onInputClick: function(ev) {
this.refs.textarea.focus();
},
onUploadClick: function(ev) {
this.refs.uploadInput.click();
},
onUploadFileSelected: function(ev) {
var files = ev.target.files;
// MessageComposer shouldn't have to rely on it's parent passing in a callback to upload a file
if (files && files.length > 0) {
this.props.uploadFile(files[0]);
}
this.refs.uploadInput.value = null;
},
onCallClick: function(ev) {
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId
});
},
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'};
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
return (
<div className="mx_MessageComposer">
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
<div className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} />
</div>
<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" 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_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>
</div>
);
}
};
});

View file

@ -16,13 +16,28 @@ limitations under the License.
'use strict';
var dis = require("../../dispatcher");
var React = require('react');
module.exports = {
onClick: function() {
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId
});
var TextForEvent = require('../../../TextForEvent');
module.exports = React.createClass({
displayName: 'TextualEvent',
statics: {
needsSenderProfile: function() {
return false;
}
},
};
render: function() {
var text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length == 0) return null;
return (
<div className="mx_EventAsTextTile">
{TextForEvent.textForEvent(this.props.mxEvent)}
</div>
);
},
});

View file

@ -18,9 +18,15 @@ limitations under the License.
var React = require('react');
module.exports = {
propTypes: {
value: React.PropTypes.number,
max: React.PropTypes.number
module.exports = React.createClass({
displayName: 'UnknownMessage',
render: function() {
var content = this.props.mxEvent.getContent();
return (
<span className="mx_UnknownMessageTile">
{content.body}
</span>
);
},
};
});

View file

@ -25,13 +25,15 @@ limitations under the License.
* 'muted': boolean,
* 'isTargetMod': boolean
*/
var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var dis = require("../../../dispatcher");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
var Modal = require("../../Modal");
var sdk = require('../../index');
module.exports = React.createClass({
displayName: 'MemberInfo',
module.exports = {
componentDidMount: function() {
// work out the current state
if (this.props.member) {
@ -320,6 +322,76 @@ module.exports = {
powerLevelContent.events_default
);
return member.powerLevel < levelToSend;
}
};
},
onCancel: function(e) {
dis.dispatch({
action: "view_user",
member: null
});
},
render: function() {
var interactButton, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) {
interactButton = <div className="mx_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>;
}
else {
interactButton = <div className="mx_MemberInfo_field" onClick={this.onChatClick}>Start chat</div>;
}
if (this.state.creatingRoom) {
var Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>;
}
if (this.state.can.kick) {
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
Kick
</div>;
}
if (this.state.can.ban) {
banButton = <div className="mx_MemberInfo_field" onClick={this.onBan}>
Ban
</div>;
}
if (this.state.can.mute) {
var muteLabel = this.state.muted ? "Unmute" : "Mute";
muteButton = <div className="mx_MemberInfo_field" onClick={this.onMuteToggle}>
{muteLabel}
</div>;
}
if (this.state.can.modifyLevel) {
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</div>
}
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
return (
<div className="mx_MemberInfo">
<img className="mx_MemberInfo_cancel" src="img/cancel-black.png" width="18" height="18" onClick={this.onCancel}/>
<div className="mx_MemberInfo_avatar">
<MemberAvatar member={this.props.member} width={48} height={48} />
</div>
<h2>{ this.props.member.name }</h2>
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
<div className="mx_MemberInfo_profileField">
power: { this.props.member.powerLevelNorm }%
</div>
<div className="mx_MemberInfo_buttons">
{interactButton}
{muteButton}
{kickButton}
{banButton}
{giveModButton}
{spinner}
</div>
</div>
);
}
});

View file

@ -0,0 +1,215 @@
/*
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 MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
var Modal = require("../../../Modal");
// The Lato WOFF doesn't include sensible combining diacritics, so Chrome chokes
// on rendering them. Revert to Arial when this happens, which on OSX works at least.
var zalgo = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/;
module.exports = React.createClass({
displayName: 'MemberTile',
getInitialState: function() {
return {};
},
onLeaveClick: function() {
var QuestionDialog = sdk.getComponent("organisms.QuestionDialog");
var roomId = this.props.member.roomId;
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: "Are you sure you want to leave the room?",
onFinished: function(should_leave) {
if (should_leave) {
var d = MatrixClientPeg.get().leave(roomId);
// FIXME: controller shouldn't be loading a view :(
var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader);
d.then(function() {
modal.close();
dis.dispatch({action: 'view_next_room'});
}, function(err) {
modal.close();
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
description: err.toString()
});
});
}
}
});
},
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state.hover !== nextState.hover) return true;
if (
this.member_last_modified_time === undefined ||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
) {
return true
}
if (
nextProps.member.user &&
(this.user_last_modified_time === undefined ||
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime())
) {
return true
}
return false;
},
mouseEnter: function(e) {
this.setState({ 'hover': true });
},
mouseLeave: function(e) {
this.setState({ 'hover': false });
},
onClick: function(e) {
dis.dispatch({
action: 'view_user',
member: this.props.member,
});
},
getDuration: function(time) {
if (!time) return;
var t = parseInt(time / 1000);
var s = t % 60;
var m = parseInt(t / 60) % 60;
var h = parseInt(t / (60 * 60)) % 24;
var d = parseInt(t / (60 * 60 * 24));
if (t < 60) {
if (t < 0) {
return "0s";
}
return s + "s";
}
if (t < 60 * 60) {
return m + "m";
}
if (t < 24 * 60 * 60) {
return h + "h";
}
return d + "d ";
},
getPrettyPresence: function(user) {
if (!user) return "Unknown";
var presence = user.presence;
if (presence === "online") return "Online";
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
if (presence === "offline") return "Offline";
return "Unknown";
},
getPowerLabel: function() {
var label = this.props.member.userId;
if (this.state.isTargetMod) {
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
}
return label;
},
render: function() {
this.member_last_modified_time = this.props.member.getLastModifiedTime();
if (this.props.member.user) {
this.user_last_modified_time = this.props.member.user.getLastModifiedTime();
}
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
var power;
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
// }
var presenceClass = "mx_MemberTile_offline";
var mainClassName = "mx_MemberTile ";
if (this.props.member.user) {
if (this.props.member.user.presence === "online") {
presenceClass = "mx_MemberTile_online";
}
else if (this.props.member.user.presence === "unavailable") {
presenceClass = "mx_MemberTile_unavailable";
}
}
mainClassName += presenceClass;
if (this.state.hover) {
mainClassName += " mx_MemberTile_hover";
}
var name = this.props.member.name;
// if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain
//var leave = isMyUser ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
var nameClass = "mx_MemberTile_name";
if (zalgo.test(name)) {
nameClass += " mx_MemberTile_zalgo";
}
var nameEl;
if (this.state.hover) {
var presence;
// FIXME: make presence data update whenever User.presence changes...
var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1;
if (active >= 0) {
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago</div>;
}
else {
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) }</div>;
}
nameEl =
<div className="mx_MemberTile_details">
<img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<div className="mx_MemberTile_userId">{ name }</div>
{ presence }
</div>
}
else {
nameEl =
<div className={nameClass}>
{ name }
</div>
}
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
return (
<div className={mainClassName} title={ this.getPowerLabel() }
onClick={ this.onClick } onMouseEnter={ this.mouseEnter }
onMouseLeave={ this.mouseLeave }>
<div className="mx_MemberTile_avatar">
<MemberAvatar member={this.props.member} width={36} height={36} />
{ power }
</div>
{ nameEl }
</div>
);
}
});

View file

@ -0,0 +1,273 @@
/*
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';
/*
* State vars:
* this.state.call_state = the UI state of the call (see CallHandler)
*/
var React = require('react');
var sdk = require('../../../index');
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
var MatrixClientPeg = require('../../../MatrixClientPeg');
module.exports = React.createClass({
displayName: 'RoomHeader',
propTypes: {
room: React.PropTypes.object,
editing: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func,
},
getDefaultProps: function() {
return {
editing: false,
onSettingsClick: function() {},
onSaveClick: function() {},
};
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
if (this.props.room) {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
var callState = call ? call.call_state : "ended";
this.setState({
call_state: callState
});
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (payload.action !== 'call_state' || !payload.room_id) {
return;
}
var call = CallHandler.getCallForRoom(payload.room_id);
var callState = call ? call.call_state : "ended";
this.setState({
call_state: callState
});
},
onVideoClick: function(e) {
dis.dispatch({
action: 'place_call',
type: e.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId
});
},
onVoiceClick: function() {
dis.dispatch({
action: 'place_call',
type: "voice",
room_id: this.props.room.roomId
});
},
onHangupClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) { return; }
dis.dispatch({
action: 'hangup',
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId
});
},
onMuteAudioClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
var newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState);
this.setState({
audioMuted: newState
});
},
onMuteVideoClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
var newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState);
this.setState({
videoMuted: newState
});
},
onNameChange: function(new_name) {
if (this.props.room.name != new_name && new_name) {
MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name);
}
},
getRoomName: function() {
return this.refs.name_edit.value;
},
onFullscreenClick: function() {
dis.dispatch({action: 'video_fullscreen', fullscreen: true}, true);
},
render: function() {
var EditableText = sdk.getComponent("elements.EditableText");
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
var header;
if (this.props.simpleHeader) {
header =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ this.props.simpleHeader }
</div>
</div>
}
else {
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
var call_buttons;
if (this.state && this.state.call_state != 'ended') {
//var muteVideoButton;
var activeCall = (
CallHandler.getCallForRoom(this.props.room.roomId)
);
/*
if (activeCall && activeCall.type === "video") {
muteVideoButton = (
<div className="mx_RoomHeader_textButton mx_RoomHeader_voipButton"
onClick={this.onMuteVideoClick}>
{
(activeCall.isLocalVideoMuted() ?
"Unmute" : "Mute") + " video"
}
</div>
);
}
{muteVideoButton}
<div className="mx_RoomHeader_textButton mx_RoomHeader_voipButton"
onClick={this.onMuteAudioClick}>
{
(activeCall && activeCall.isMicrophoneMuted() ?
"Unmute" : "Mute") + " audio"
}
</div>
*/
call_buttons = (
<div className="mx_RoomHeader_textButton"
onClick={this.onHangupClick}>
End call
</div>
);
}
var name = null;
var topic_el = null;
var cancel_button = null;
var save_button = null;
var settings_button = null;
var actual_name = this.props.room.currentState.getStateEvents('m.room.name', '');
if (actual_name) actual_name = actual_name.getContent().name;
if (this.props.editing) {
name =
<div className="mx_RoomHeader_nameEditing">
<input className="mx_RoomHeader_nameInput" type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/>
</div>
// if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div>
cancel_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onCancelClick}>Cancel</div>
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save Changes</div>
} else {
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<div className="mx_RoomHeader_nametext">{ this.props.room.name }</div>
<div className="mx_RoomHeader_settingsButton">
<img src="img/settings.png" width="12" height="12"/>
</div>
</div>
if (topic) topic_el = <div className="mx_RoomHeader_topic" title={topic.getContent().topic}>{ topic.getContent().topic }</div>;
}
var roomAvatar = null;
if (this.props.room) {
roomAvatar = (
<RoomAvatar room={this.props.room} width="48" height="48" />
);
}
var zoom_button, video_button, voice_button;
if (activeCall) {
if (activeCall.type == "video") {
zoom_button = (
<div className="mx_RoomHeader_button" onClick={this.onFullscreenClick}>
<img src="img/zoom.png" title="Fullscreen" alt="Fullscreen" width="32" height="32" style={{ 'marginTop': '-5px' }}/>
</div>
);
}
video_button =
<div className="mx_RoomHeader_button mx_RoomHeader_video" onClick={activeCall && activeCall.type === "video" ? this.onMuteVideoClick : this.onVideoClick}>
<img src="img/video.png" title="Video call" alt="Video call" width="32" height="32" style={{ 'marginTop': '-8px' }}/>
</div>;
voice_button =
<div className="mx_RoomHeader_button mx_RoomHeader_voice" onClick={activeCall ? this.onMuteAudioClick : this.onVoiceClick}>
<img src="img/voip.png" title="VoIP call" alt="VoIP call" width="32" height="32" style={{ 'marginTop': '-8px' }}/>
</div>;
}
header =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
{ roomAvatar }
</div>
<div className="mx_RoomHeader_info">
{ name }
{ topic_el }
</div>
</div>
{call_buttons}
{cancel_button}
{save_button}
<div className="mx_RoomHeader_rightRow">
{ video_button }
{ voice_button }
{ zoom_button }
<div className="mx_RoomHeader_button">
<img src="img/search.png" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>
</div>
</div>
</div>
}
return (
<div className="mx_RoomHeader">
{ header }
</div>
);
},
});

View file

@ -0,0 +1,237 @@
/*
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.
*/
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'RoomSettings',
propTypes: {
room: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
power_levels_changed: false
};
},
getTopic: function() {
return this.refs.topic.value;
},
getJoinRules: function() {
return this.refs.is_private.checked ? "invite" : "public";
},
getHistoryVisibility: function() {
return this.refs.share_history.checked ? "shared" : "invited";
},
getPowerLevels: function() {
if (!this.state.power_levels_changed) return undefined;
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
power_levels = power_levels.getContent();
var new_power_levels = {
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,
};
return new_power_levels;
},
onPowerLevelsChanged: function() {
this.setState({
power_levels_changed: true
});
},
render: function() {
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
if (topic) topic = topic.getContent().topic;
var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
if (join_rule) join_rule = join_rule.getContent().join_rule;
var history_visibility = this.props.room.currentState.getStateEvents('m.room.history_visibility', '');
if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
var events_levels = power_levels.events || {};
if (power_levels) {
power_levels = power_levels.getContent();
var ban_level = parseInt(power_levels.ban);
var kick_level = parseInt(power_levels.kick);
var redact_level = parseInt(power_levels.redact);
var invite_level = parseInt(power_levels.invite || 0);
var send_level = parseInt(power_levels.events_default || 0);
var state_level = parseInt(power_levels.state_default || 0);
var default_user_level = parseInt(power_levels.users_default || 0);
if (power_levels.ban == undefined) ban_level = 50;
if (power_levels.kick == undefined) kick_level = 50;
if (power_levels.redact == undefined) redact_level = 50;
var user_levels = power_levels.users || {};
var user_id = MatrixClientPeg.get().credentials.userId;
var current_user_level = user_levels[user_id];
if (current_user_level == undefined) current_user_level = default_user_level;
var power_level_level = events_levels["m.room.power_levels"];
if (power_level_level == undefined) {
power_level_level = state_level;
}
var can_change_levels = current_user_level >= power_level_level;
} else {
var ban_level = 50;
var kick_level = 50;
var redact_level = 50;
var invite_level = 0;
var send_level = 0;
var state_level = 0;
var default_user_level = 0;
var user_levels = [];
var events_levels = [];
var current_user_level = 0;
var power_level_level = 0;
var can_change_levels = false;
}
var room_avatar_level = parseInt(power_levels.state_default || 0);
if (events_levels['m.room.avatar'] !== undefined) {
room_avatar_level = events_levels['m.room.avatar'];
}
var can_set_room_avatar = current_user_level >= room_avatar_level;
var change_avatar;
if (can_set_room_avatar) {
change_avatar = <div>
<h3>Room Icon</h3>
<ChangeAvatar room={this.props.room} />
</div>;
}
var banned = this.props.room.getMembersWithMembership("ban");
return (
<div className="mx_RoomSettings">
<textarea className="mx_RoomSettings_description" placeholder="Topic" defaultValue={topic} ref="topic"/> <br/>
<label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/>
<label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/>
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <br/>
<h3>Power levels</h3>
<div className="mx_RoomSettings_power_levels mx_RoomSettings_settings">
<div>
<label htmlFor="mx_RoomSettings_ban_level">Ban level</label>
<input type="text" defaultValue={ban_level} size="3" ref="ban" id="mx_RoomSettings_ban_level"
disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div>
<label htmlFor="mx_RoomSettings_kick_level">Kick level</label>
<input type="text" defaultValue={kick_level} size="3" ref="kick" id="mx_RoomSettings_kick_level"
disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div>
<label htmlFor="mx_RoomSettings_redact_level">Redact level</label>
<input type="text" defaultValue={redact_level} size="3" ref="redact" id="mx_RoomSettings_redact_level"
disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div>
<label htmlFor="mx_RoomSettings_invite_level">Invite level</label>
<input type="text" defaultValue={invite_level} size="3" ref="invite" id="mx_RoomSettings_invite_level"
disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div>
<label htmlFor="mx_RoomSettings_event_level">Send event level</label>
<input type="text" defaultValue={send_level} size="3" ref="events_default" id="mx_RoomSettings_event_level"
disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div>
<label htmlFor="mx_RoomSettings_state_level">Set state level</label>
<input type="text" defaultValue={state_level} size="3" ref="state_default" id="mx_RoomSettings_state_level"
disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div>
<label htmlFor="mx_RoomSettings_user_level">Default user level</label>
<input type="text" defaultValue={default_user_level} size="3" ref="users_default"
id="mx_RoomSettings_user_level" disabled={!can_change_levels || current_user_level < default_user_level}
onChange={this.onPowerLevelsChanged}/>
</div>
</div>
<h3>User levels</h3>
<div className="mx_RoomSettings_user_levels mx_RoomSettings_settings">
{Object.keys(user_levels).map(function(user, i) {
return (
<div key={user}>
<label htmlFor={"mx_RoomSettings_user_"+i}>{user}</label>
<input type="text" defaultValue={user_levels[user]} size="3" id={"mx_RoomSettings_user_"+i} disabled/>
</div>
);
})}
</div>
<h3>Event levels</h3>
<div className="mx_RoomSettings_event_lvels mx_RoomSettings_settings">
{Object.keys(events_levels).map(function(event_type, i) {
return (
<div key={event_type}>
<label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
<input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
</div>
);
})}
</div>
<h3>Banned users</h3>
<div className="mx_RoomSettings_banned">
{banned.map(function(member, i) {
return (
<div key={i}>
{member.userId}
</div>
);
})}
</div>
{change_avatar}
</div>
);
}
});

View file

@ -0,0 +1,142 @@
/*
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 classNames = require('classnames');
var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'RoomTile',
propTypes: {
// TODO: We should *optionally* support DND stuff and ideally be impl agnostic about it
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 });
},
onClick: function() {
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId
});
},
onMouseEnter: function() {
this.setState( { hover : true });
},
onMouseLeave: function() {
this.setState( { hover : false });
},
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': (me && me.membership == 'invite'),
});
var name;
if (this.props.isInvite) {
name = this.props.room.getMember(myUserId).events.member.getSender();
}
else {
// 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
var badge;
if (this.props.highlight) {
badge = <div className="mx_RoomTile_badge"/>;
}
/*
if (this.props.highlight) {
badge = <div className="mx_RoomTile_badge">!</div>;
}
else if (this.props.unread) {
badge = <div className="mx_RoomTile_badge">1</div>;
}
var nameCell;
if (badge) {
nameCell = <div className="mx_RoomTile_nameBadge"><div className="mx_RoomTile_name">{name}</div><div className="mx_RoomTile_badgeCell">{badge}</div></div>;
}
else {
nameCell = <div className="mx_RoomTile_name">{name}</div>;
}
*/
var label;
if (!this.props.collapsed) {
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
label = <div className={ className }>{name}</div>;
}
else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("molecules.RoomTooltip");
label = <RoomTooltip room={this.props.room}/>;
}
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
// 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" />
{ badge }
</div>
{ label }
</div>
));
}
});

View file

@ -15,9 +15,11 @@ limitations under the License.
*/
var React = require('react');
var MatrixClientPeg = require("../../MatrixClientPeg");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var sdk = require('../../../index');
module.exports = {
module.exports = React.createClass({
displayName: 'ChangeAvatar',
propTypes: {
initialAvatarUrl: React.PropTypes.string,
room: React.PropTypes.object,
@ -77,4 +79,53 @@ module.exports = {
self.onError(error);
});
},
}
onFileSelected: function(ev) {
this.avatarSet = true;
this.setAvatarFromFile(ev.target.files[0]);
},
onError: function(error) {
this.setState({
errorText: "Failed to upload profile picture!"
});
},
render: function() {
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
var avatarImg;
// Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) {
avatarImg = <RoomAvatar room={this.props.room} width='320' height='240' resizeMethod='scale' />;
} else {
var style = {
maxWidth: 320,
maxHeight: 240,
};
avatarImg = <img src={this.state.avatarUrl} style={style} />;
}
switch (this.state.phase) {
case this.Phases.Display:
case this.Phases.Error:
return (
<div>
<div className="mx_Dialog_content">
{avatarImg}
</div>
<div className="mx_Dialog_content">
Upload new:
<input type="file" onChange={this.onFileSelected}/>
{this.state.errorText}
</div>
</div>
);
case this.Phases.Uploading:
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
}
}
});

View file

@ -16,9 +16,11 @@ limitations under the License.
'use strict';
var React = require('react');
var MatrixClientPeg = require("../../MatrixClientPeg");
var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
module.exports = {
module.exports = React.createClass({
displayName: 'ChangeDisplayName',
propTypes: {
onFinished: React.PropTypes.func
},
@ -72,4 +74,32 @@ module.exports = {
});
});
},
}
edit: function() {
this.refs.displayname_edit.edit()
},
onValueChanged: function(new_value, shouldSubmit) {
if (shouldSubmit) {
this.changeDisplayname(new_value);
}
},
render: function() {
if (this.state.busy) {
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
} else if (this.state.errorString) {
return (
<div className="error">{this.state.errorString}</div>
);
} else {
var EditableText = sdk.getComponent('elements.EditableText');
return (
<EditableText ref="displayname_edit" initialValue={this.state.displayName} label="Click to set display name." onValueChanged={this.onValueChanged}/>
);
}
}
});

View file

@ -0,0 +1,135 @@
/*
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 MatrixClientPeg = require("../../../MatrixClientPeg");
module.exports = React.createClass({
displayName: 'ChangePassword',
propTypes: {
onFinished: React.PropTypes.func,
},
Phases: {
Edit: "edit",
Uploading: "uploading",
Error: "error",
Success: "Success"
},
getDefaultProps: function() {
return {
onFinished: function() {},
};
},
getInitialState: function() {
return {
phase: this.Phases.Edit,
errorString: ''
}
},
changePassword: function(old_password, new_password) {
var cli = MatrixClientPeg.get();
var authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
password: old_password
};
this.setState({
phase: this.Phases.Uploading,
errorString: '',
})
var d = cli.setPassword(authDict, new_password);
var self = this;
d.then(function() {
self.setState({
phase: self.Phases.Success,
errorString: '',
})
}, function(err) {
self.setState({
phase: self.Phases.Error,
errorString: err.toString()
})
});
},
onClickChange: function() {
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,
errorString: "Passwords don't match"
});
} else if (new_password == '' || old_password == '') {
this.setState({
state: this.Phases.Error,
errorString: "Passwords can't be empty"
});
} else {
this.changePassword(old_password, new_password);
}
},
render: function() {
switch (this.state.phase) {
case this.Phases.Edit:
case this.Phases.Error:
return (
<div>
<div className="mx_Dialog_content">
<div>{this.state.errorString}</div>
<div><label>Old password <input type="password" ref="old_input"/></label></div>
<div><label>New password <input type="password" ref="new_input"/></label></div>
<div><label>Confirm password <input type="password" ref="confirm_input"/></label></div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onClickChange}>Change Password</button>
<button onClick={this.props.onFinished}>Cancel</button>
</div>
</div>
);
case this.Phases.Uploading:
var Loader = sdk.getComponent("elements.Spinner");
return (
<div className="mx_Dialog_content">
<Loader />
</div>
);
case this.Phases.Success:
return (
<div>
<div className="mx_Dialog_content">
Success!
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished}>Ok</button>
</div>
</div>
)
}
}
});

View file

@ -1,76 +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 MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
propTypes: {
onFinished: React.PropTypes.func,
},
Phases: {
Edit: "edit",
Uploading: "uploading",
Error: "error",
Success: "Success"
},
getDefaultProps: function() {
return {
onFinished: function() {},
};
},
getInitialState: function() {
return {
phase: this.Phases.Edit,
errorString: ''
}
},
changePassword: function(old_password, new_password) {
var cli = MatrixClientPeg.get();
var authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
password: old_password
};
this.setState({
phase: this.Phases.Uploading,
errorString: '',
})
var d = cli.setPassword(authDict, new_password);
var self = this;
d.then(function() {
self.setState({
phase: self.Phases.Success,
errorString: '',
})
}, function(err) {
self.setState({
phase: self.Phases.Error,
errorString: err.toString()
})
});
},
}

View file

@ -1,28 +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 MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
shouldHighlight: function() {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
return actions.tweaks.highlight;
}
};

View file

@ -1,28 +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 linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../linkify-matrix.js');
linkifyMatrix(linkify);
module.exports = {
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
}
};

View file

@ -1,30 +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 linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../linkify-matrix');
linkifyMatrix(linkify);
module.exports = {
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
}
};

View file

@ -1,59 +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 dis = require("../../dispatcher");
var Modal = require("../../Modal");
var sdk = require('../../index.js');
var MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
getInitialState: function() {
return {};
},
onLeaveClick: function() {
var QuestionDialog = sdk.getComponent("organisms.QuestionDialog");
var roomId = this.props.member.roomId;
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: "Are you sure you want to leave the room?",
onFinished: function(should_leave) {
if (should_leave) {
var d = MatrixClientPeg.get().leave(roomId);
// FIXME: controller shouldn't be loading a view :(
var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader);
d.then(function() {
modal.close();
dis.dispatch({action: 'view_next_room'});
}, function(err) {
modal.close();
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
description: err.toString()
});
});
}
}
});
}
};

View file

@ -1,23 +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 MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
};

View file

@ -1,118 +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';
/*
* State vars:
* this.state.call_state = the UI state of the call (see CallHandler)
*/
var React = require('react');
var dis = require("../../dispatcher");
var CallHandler = require("../../CallHandler");
module.exports = {
propTypes: {
room: React.PropTypes.object,
editing: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func,
},
getDefaultProps: function() {
return {
editing: false,
onSettingsClick: function() {},
onSaveClick: function() {},
};
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
if (this.props.room) {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
var callState = call ? call.call_state : "ended";
this.setState({
call_state: callState
});
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (payload.action !== 'call_state' || !payload.room_id) {
return;
}
var call = CallHandler.getCallForRoom(payload.room_id);
var callState = call ? call.call_state : "ended";
this.setState({
call_state: callState
});
},
onVideoClick: function(e) {
dis.dispatch({
action: 'place_call',
type: e.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId
});
},
onVoiceClick: function() {
dis.dispatch({
action: 'place_call',
type: "voice",
room_id: this.props.room.roomId
});
},
onHangupClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) { return; }
dis.dispatch({
action: 'hangup',
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId
});
},
onMuteAudioClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
var newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState);
this.setState({
audioMuted: newState
});
},
onMuteVideoClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
var newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState);
this.setState({
videoMuted: newState
});
}
};