diff --git a/package.json b/package.json
index 2da0968345..0b5a18168f 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js
new file mode 100644
index 0000000000..a7b1849e18
--- /dev/null
+++ b/src/ContextualMenu.js
@@ -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 =
+ position.left = props.left + 8;
+ } else {
+ chevron =
+ 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 = (
+
+
+ {chevron}
+
+
+
+
+ );
+
+ ReactDOM.render(menu, this.getOrCreateContainer());
+
+ return {close: closeMenu};
+ },
+};
diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
new file mode 100644
index 0000000000..824f59ab20
--- /dev/null
+++ b/src/HtmlUtils.js
@@ -0,0 +1,108 @@
+/*
+Copyright 2015 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+var React = require('react');
+var sanitizeHtml = require('sanitize-html');
+var highlight = require('highlight.js');
+
+var sanitizeHtmlParams = {
+ allowedTags: [
+ 'font', // custom to matrix. deliberately no h1/h2 to stop people shouting.
+ 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
+ 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
+ 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
+ ],
+ allowedAttributes: {
+ // custom ones first:
+ font: [ 'color' ], // custom to matrix
+ a: [ 'href', 'name', 'target' ], // remote target: custom to matrix
+ // We don't currently allow img itself by default, but this
+ // would make sense if we did
+ img: [ 'src' ],
+ },
+ // Lots of these won't come up by default because we don't allow them
+ selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
+ // URL schemes we permit
+ allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
+ allowedSchemesByTag: {},
+
+ transformTags: { // custom to matrix
+ // add blank targets to all hyperlinks
+ 'a': sanitizeHtml.simpleTransform('a', { target: '_blank'} )
+ },
+};
+
+module.exports = {
+ bodyToHtml: function(content, searchTerm) {
+ var originalBody = content.body;
+ var body;
+
+ if (searchTerm) {
+ var lastOffset = 0;
+ var bodyList = [];
+ var k = 0;
+ var offset;
+
+ // XXX: rather than searching for the search term in the body,
+ // we should be looking at the match delimiters returned by the FTS engine
+ if (content.format === "org.matrix.custom.html") {
+
+ var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
+ var safeSearchTerm = sanitizeHtml(searchTerm, sanitizeHtmlParams);
+ while ((offset = safeBody.indexOf(safeSearchTerm, lastOffset)) >= 0) {
+ // FIXME: we need to apply the search highlighting to only the text elements of HTML, which means
+ // hooking into the sanitizer parser rather than treating it as a string. Otherwise
+ // the act of highlighting a or whatever will break the HTML badly.
+ bodyList.push();
+ bodyList.push();
+ lastOffset = offset + safeSearchTerm.length;
+ }
+ bodyList.push();
+ }
+ else {
+ while ((offset = originalBody.indexOf(searchTerm, lastOffset)) >= 0) {
+ bodyList.push({ originalBody.substring(lastOffset, offset) });
+ bodyList.push({ searchTerm });
+ lastOffset = offset + searchTerm.length;
+ }
+ bodyList.push({ originalBody.substring(lastOffset) });
+ }
+ body = bodyList;
+ }
+ else {
+ if (content.format === "org.matrix.custom.html") {
+ var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
+ body = ;
+ }
+ else {
+ body = originalBody;
+ }
+ }
+
+ return body;
+ },
+
+ highlightDom: function(element) {
+ var blocks = element.getElementsByTagName("code");
+ for (var i = 0; i < blocks.length; i++) {
+ highlight.highlightBlock(blocks[i]);
+ }
+ },
+
+}
+
diff --git a/src/Velociraptor.js b/src/Velociraptor.js
new file mode 100644
index 0000000000..d973a17f7f
--- /dev/null
+++ b/src/Velociraptor.js
@@ -0,0 +1,113 @@
+var React = require('react');
+var ReactDom = require('react-dom');
+var Velocity = require('velocity-animate');
+
+/**
+ * The Velociraptor contains components and animates transitions with velocity.
+ * It will only pick up direct changes to properties ('left', currently), and so
+ * will not work for animating positional changes where the position is implicit
+ * from DOM order. This makes it a lot simpler and lighter: if you need fully
+ * automatic positional animation, look at react-shuffle or similar libraries.
+ */
+module.exports = React.createClass({
+ displayName: 'Velociraptor',
+
+ propTypes: {
+ children: React.PropTypes.array,
+ transition: React.PropTypes.object,
+ container: React.PropTypes.string
+ },
+
+ componentWillMount: function() {
+ this.children = {};
+ this.nodes = {};
+ var self = this;
+ React.Children.map(this.props.children, function(c) {
+ self.children[c.key] = c;
+ });
+ },
+
+ componentWillReceiveProps: function(nextProps) {
+ var self = this;
+ var oldChildren = this.children;
+ this.children = {};
+ React.Children.map(nextProps.children, function(c) {
+ if (oldChildren[c.key]) {
+ var old = oldChildren[c.key];
+ var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
+
+ if (oldNode.style.left != c.props.style.left) {
+ Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
+ // special case visibility because it's nonsensical to animate an invisible element
+ // so we always hidden->visible pre-transition and visible->hidden after
+ if (oldNode.style.visibility == 'visible' && c.props.style.visibility == 'hidden') {
+ oldNode.style.visibility = c.props.style.visibility;
+ }
+ });
+ if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
+ oldNode.style.visibility = c.props.style.visibility;
+ }
+ //console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
+ }
+ self.children[c.key] = old;
+ } else {
+ // new element. If it has a startStyle, use that as the style and go through
+ // the enter animations
+ var newProps = {
+ ref: self.collectNode.bind(self, c.key)
+ };
+ if (c.props.startStyle && Object.keys(c.props.startStyle).length) {
+ var startStyle = c.props.startStyle;
+ if (Array.isArray(startStyle)) {
+ startStyle = startStyle[0];
+ }
+ newProps._restingStyle = c.props.style;
+ newProps.style = startStyle;
+ //console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
+ // apply the enter animations once it's mounted
+ }
+ self.children[c.key] = React.cloneElement(c, newProps);
+ }
+ });
+ },
+
+ collectNode: function(k, node) {
+ if (
+ this.nodes[k] === undefined &&
+ node.props.startStyle &&
+ Object.keys(node.props.startStyle).length
+ ) {
+ var domNode = ReactDom.findDOMNode(node);
+ var startStyles = node.props.startStyle;
+ var transitionOpts = node.props.enterTransitionOpts;
+ if (!Array.isArray(startStyles)) {
+ startStyles = [ startStyles ];
+ transitionOpts = [ transitionOpts ];
+ }
+ // start from startStyle 1: 0 is the one we gave it
+ // to start with, so now we animate 1 etc.
+ for (var i = 1; i < startStyles.length; ++i) {
+ Velocity(domNode, startStyles[i], transitionOpts[i-1]);
+ //console.log("start: "+JSON.stringify(startStyles[i]));
+ }
+ // and then we animate to the resting state
+ Velocity(domNode, node.props._restingStyle, transitionOpts[i-1]);
+ //console.log("enter: "+JSON.stringify(node.props._restingStyle));
+ }
+ this.nodes[k] = node;
+ },
+
+ render: function() {
+ var self = this;
+ var childList = Object.keys(this.children).map(function(k) {
+ return React.cloneElement(self.children[k], {
+ ref: self.collectNode.bind(self, self.children[k].key)
+ });
+ });
+ return (
+
+ {childList}
+
+ );
+ },
+});
diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js
new file mode 100644
index 0000000000..c85aa254fa
--- /dev/null
+++ b/src/VelocityBounce.js
@@ -0,0 +1,15 @@
+var Velocity = require('velocity-animate');
+
+// courtesy of https://github.com/julianshapiro/velocity/issues/283
+// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
+function bounce( p ) {
+ var pow2,
+ bounce = 4;
+
+ while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
+ return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
+}
+
+Velocity.Easings.easeOutBounce = function(p) {
+ return 1 - bounce(1 - p);
+}
diff --git a/src/controllers/molecules/RoomSettings.js b/src/components/views/elements/ProgressBar.js
similarity index 53%
rename from src/controllers/molecules/RoomSettings.js
rename to src/components/views/elements/ProgressBar.js
index 3c0682d09a..bab6a701dd 100644
--- a/src/controllers/molecules/RoomSettings.js
+++ b/src/components/views/elements/ProgressBar.js
@@ -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 (
+
+ );
}
-};
+});
\ No newline at end of file
diff --git a/src/controllers/molecules/UserSelector.js b/src/components/views/elements/UserSelector.js
similarity index 56%
rename from src/controllers/molecules/UserSelector.js
rename to src/components/views/elements/UserSelector.js
index 67a56163fa..ea04de59a9 100644
--- a/src/controllers/molecules/UserSelector.js
+++ b/src/components/views/elements/UserSelector.js
@@ -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 (
+
+ );
+ }
+});
diff --git a/src/components/views/messages/Event.js b/src/components/views/messages/Event.js
new file mode 100644
index 0000000000..2fb2917541
--- /dev/null
+++ b/src/components/views/messages/Event.js
@@ -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(
+
+ );
+ // 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 = { remainder }+
+ ;
+ left -= 15;
+ }
+ editButton = (
+
+ );
+ }
+
+ return
+ { editButton }
+ { remText }
+
+ { avatars }
+
+ ;
+ },
+
+ 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 =
+
+ 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 = (
+
+
+ );
+ } else if (content.body) {
+ return (
+
+ Image '{content.body}' cannot be displayed.
+
+ );
+ } else {
+ return (
+
+ This image cannot be displayed.
+
+ );
+ }
+ },
+});
diff --git a/src/components/views/messages/MNoticeMessage.js b/src/components/views/messages/MNoticeMessage.js
new file mode 100644
index 0000000000..3a89d1ff6a
--- /dev/null
+++ b/src/components/views/messages/MNoticeMessage.js
@@ -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 (
+
+ { body }
+
+ );
+ },
+});
+
diff --git a/src/components/views/messages/MRoomMemberEvent.js b/src/components/views/messages/MRoomMemberEvent.js
new file mode 100644
index 0000000000..6e73519f2e
--- /dev/null
+++ b/src/components/views/messages/MRoomMemberEvent.js
@@ -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 ? : null;
+ var text = this.getMemberEventText();
+ if (!text) return ;
+ var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
+ var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
+ return (
+
+
+
+
+ { timestamp }
+
+
+ { text }
+
+
+ );
+ },
+});
+
diff --git a/src/components/views/messages/MTextMessage.js b/src/components/views/messages/MTextMessage.js
new file mode 100644
index 0000000000..d3b337cbc1
--- /dev/null
+++ b/src/components/views/messages/MTextMessage.js
@@ -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 (
+
+ { body }
+
+ );
+ },
+});
+
diff --git a/src/components/views/messages/MVideoMessage.js b/src/components/views/messages/MVideoMessage.js
new file mode 100644
index 0000000000..5771ed2172
--- /dev/null
+++ b/src/components/views/messages/MVideoMessage.js
@@ -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 (
+
+
+
+ );
+ },
+});
diff --git a/src/components/views/messages/Message.js b/src/components/views/messages/Message.js
new file mode 100644
index 0000000000..fa74a8e137
--- /dev/null
+++ b/src/components/views/messages/Message.js
@@ -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 ;
+ },
+});
diff --git a/src/controllers/molecules/MessageComposer.js b/src/components/views/messages/MessageComposer.js
similarity index 85%
rename from src/controllers/molecules/MessageComposer.js
rename to src/components/views/messages/MessageComposer.js
index 237c710395..869e9f7614 100644
--- a/src/controllers/molecules/MessageComposer.js
+++ b/src/components/views/messages/MessageComposer.js
@@ -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 (
+
+ );
+ }
+
+ 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 =
+
+
+
+ // if (topic) topic_el =
+ cancel_button =
Cancel
+ save_button =
Save Changes
+ } else {
+ //
+ name =
+
+
{ this.props.room.name }
+
+
+
+
+ if (topic) topic_el =
{ topic.getContent().topic }
;
+ }
+
+ var roomAvatar = null;
+ if (this.props.room) {
+ roomAvatar = (
+
+ );
+ }
+
+ var zoom_button, video_button, voice_button;
+ if (activeCall) {
+ if (activeCall.type == "video") {
+ zoom_button = (
+
+ );
+ }
+});
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js
new file mode 100644
index 0000000000..b9233f2a32
--- /dev/null
+++ b/src/components/views/rooms/RoomTile.js
@@ -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 ;
+ }
+*/
+
+ 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 = ;
+ }
+ /*
+ if (this.props.highlight) {
+ badge =
!
;
+ }
+ else if (this.props.unread) {
+ badge =
1
;
+ }
+ var nameCell;
+ if (badge) {
+ nameCell =
{name}
{badge}
;
+ }
+ else {
+ nameCell =
{name}
;
+ }
+ */
+
+ var label;
+ if (!this.props.collapsed) {
+ var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
+ label =
{name}
;
+ }
+ else if (this.state.hover) {
+ var RoomTooltip = sdk.getComponent("molecules.RoomTooltip");
+ label = ;
+ }
+
+ 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(
+
+
+
+ { badge }
+
+ { label }
+
+ ));
+ }
+});
diff --git a/src/controllers/molecules/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
similarity index 54%
rename from src/controllers/molecules/ChangeAvatar.js
rename to src/components/views/settings/ChangeAvatar.js
index 7e8f959ebf..2ae50a0cae 100644
--- a/src/controllers/molecules/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -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 = ;
+ } else {
+ var style = {
+ maxWidth: 320,
+ maxHeight: 240,
+ };
+ avatarImg = ;
+ }
+
+ switch (this.state.phase) {
+ case this.Phases.Display:
+ case this.Phases.Error:
+ return (
+
+ );
+ } else {
+ var EditableText = sdk.getComponent('elements.EditableText');
+ return (
+
+ );
+ }
+ }
+});
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
new file mode 100644
index 0000000000..a6666b7ed1
--- /dev/null
+++ b/src/components/views/settings/ChangePassword.js
@@ -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 (
+
+
+
{this.state.errorString}
+
+
+
+
+
+
+
+
+
+ );
+ case this.Phases.Uploading:
+ var Loader = sdk.getComponent("elements.Spinner");
+ return (
+
+
+
+ );
+ case this.Phases.Success:
+ return (
+
+
+ Success!
+
+
+
+
+
+ )
+ }
+ }
+});
diff --git a/src/controllers/molecules/ChangePassword.js b/src/controllers/molecules/ChangePassword.js
deleted file mode 100644
index 637e133a79..0000000000
--- a/src/controllers/molecules/ChangePassword.js
+++ /dev/null
@@ -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()
- })
- });
- },
-}
diff --git a/src/controllers/molecules/EventTile.js b/src/controllers/molecules/EventTile.js
deleted file mode 100644
index 953e33b516..0000000000
--- a/src/controllers/molecules/EventTile.js
+++ /dev/null
@@ -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;
- }
-};
-
diff --git a/src/controllers/molecules/MNoticeTile.js b/src/controllers/molecules/MNoticeTile.js
deleted file mode 100644
index 597ce3cd10..0000000000
--- a/src/controllers/molecules/MNoticeTile.js
+++ /dev/null
@@ -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);
- }
-};
diff --git a/src/controllers/molecules/MTextTile.js b/src/controllers/molecules/MTextTile.js
deleted file mode 100644
index d32d8ae911..0000000000
--- a/src/controllers/molecules/MTextTile.js
+++ /dev/null
@@ -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);
- }
-};
-
diff --git a/src/controllers/molecules/MemberTile.js b/src/controllers/molecules/MemberTile.js
deleted file mode 100644
index 057bc82497..0000000000
--- a/src/controllers/molecules/MemberTile.js
+++ /dev/null
@@ -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()
- });
- });
- }
- }
- });
- }
-};
diff --git a/src/controllers/molecules/MessageTile.js b/src/controllers/molecules/MessageTile.js
deleted file mode 100644
index 7f3416d6db..0000000000
--- a/src/controllers/molecules/MessageTile.js
+++ /dev/null
@@ -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 = {
-};
-
diff --git a/src/controllers/molecules/RoomHeader.js b/src/controllers/molecules/RoomHeader.js
deleted file mode 100644
index 2e61f0608c..0000000000
--- a/src/controllers/molecules/RoomHeader.js
+++ /dev/null
@@ -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
- });
- }
-};