diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js
new file mode 100644
index 0000000000..5adde400a6
--- /dev/null
+++ b/src/components/views/rooms/MemberTile.js
@@ -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 = ;
+ // }
+ 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 ? : 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 =
{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago
;
+ }
+ else {
+ presence = { this.getPrettyPresence(this.props.member.user) }
;
+ }
+
+ nameEl =
+
+
+
{ name }
+ { presence }
+
+ }
+ else {
+ nameEl =
+
+ { name }
+
+ }
+
+ var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
+ return (
+
+
+
+ { power }
+
+ { nameEl }
+
+ );
+ }
+});
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 = ;
+ }
+ 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/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/RoomTile.js b/src/controllers/molecules/RoomTile.js
deleted file mode 100644
index 78927ec55e..0000000000
--- a/src/controllers/molecules/RoomTile.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 dis = require("../../dispatcher");
-
-module.exports = {
- onClick: function() {
- dis.dispatch({
- action: 'view_room',
- room_id: this.props.room.roomId
- });
- },
-};