diff --git a/skins/base/views/atoms/EditableText.js b/skins/base/views/atoms/EditableText.js
index 07bdc911..a4508744 100644
--- a/skins/base/views/atoms/EditableText.js
+++ b/skins/base/views/atoms/EditableText.js
@@ -44,7 +44,7 @@ module.exports = React.createClass({
 
     onFinish: function(ev) {
         if (ev.target.value) {
-            this.setValue(ev.target.value);
+            this.setValue(ev.target.value, ev.key === "Enter");
         } else {
             this.cancelEdit();
         }
diff --git a/skins/base/views/molecules/MemberInfo.js b/skins/base/views/molecules/MemberInfo.js
index 19e0dc46..d8559dbb 100644
--- a/skins/base/views/molecules/MemberInfo.js
+++ b/skins/base/views/molecules/MemberInfo.js
@@ -19,11 +19,33 @@ limitations under the License.
 var React = require('react');
 
 var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
-//var MemberInfoController = require("../../../../src/controllers/molecules/MemberInfo");
+var MemberInfoController = require("../../../../src/controllers/molecules/MemberInfo");
 
 module.exports = React.createClass({
     displayName: 'MemberInfo',
-    //mixins: [MemberInfoController],
+    mixins: [MemberInfoController],
+
+    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 ";
+    },
 
     render: function() {
         var power;
@@ -31,6 +53,10 @@ module.exports = React.createClass({
             var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
             power = <img src={ img } className="mx_MemberTile_power" width="48" height="48" alt=""/>;
         }
+        var activeAgo = "unknown";
+        if (this.state.active >= 0) {
+            activeAgo = this.getDuration(this.state.active);
+        }
 
         return (
             <div className="mx_MemberInfo">
@@ -41,9 +67,9 @@ module.exports = React.createClass({
                          width="128" height="128" alt=""/>
                 </div>
                 <div className="mx_MemberInfo_field">{this.props.member.userId}</div>
-                <div className="mx_MemberInfo_field">Presence: {this.props.member.presence}</div>
-                <div className="mx_MemberInfo_field">Last active: {this.props.member.last_active_ago}</div>
-                <div className="mx_MemberInfo_button">Start chat</div>
+                <div className="mx_MemberInfo_field">Presence: {this.state.presence}</div>
+                <div className="mx_MemberInfo_field">Last active: {activeAgo}</div>
+                <div className="mx_MemberInfo_button" onClick={this.onChatClick}>Start chat</div>
             </div>
         );
     }
diff --git a/skins/base/views/organisms/MemberList.js b/skins/base/views/organisms/MemberList.js
index 590ebb55..9283a928 100644
--- a/skins/base/views/organisms/MemberList.js
+++ b/skins/base/views/organisms/MemberList.js
@@ -23,6 +23,7 @@ var MemberListController = require("../../../../src/controllers/organisms/Member
 var ComponentBroker = require('../../../../src/ComponentBroker');
 
 var MemberTile = ComponentBroker.get("molecules/MemberTile");
+var EditableText = ComponentBroker.get("atoms/EditableText");
 
 
 module.exports = React.createClass({
@@ -39,6 +40,31 @@ module.exports = React.createClass({
         });
     },
 
+    onPopulateInvite: function(inputText, shouldSubmit) {
+        // reset back to placeholder
+        this.refs.invite.setValue("Invite", false, true);
+        if (!shouldSubmit) {
+            return; // enter key wasn't pressed
+        }
+        this.onInvite(inputText);
+    },
+
+    inviteTile: function() {
+        if (this.state.inviting) {
+            return (
+                <div></div>
+            );
+        }
+        return (
+            <div className="mx_MemberTile">
+                <div className="mx_MemberTile_avatar"><img src="img/create-big.png" width="40" height="40" alt=""/></div>            
+                <div className="mx_MemberTile_name">
+                    <EditableText ref="invite" placeHolder="Invite" onValueChanged={this.onPopulateInvite}/>
+                </div>
+            </div>
+        );
+    },
+
     render: function() {
         return (
             <div className="mx_MemberList">
@@ -49,10 +75,7 @@ module.exports = React.createClass({
                     <h2>Members</h2>
                     <div className="mx_MemberList_wrapper">
                         {this.makeMemberTiles()}
-                        <div className="mx_MemberTile">
-                            <div className="mx_MemberTile_avatar"><img src="img/create-big.png" width="40" height="40" alt=""/></div>            
-                            <div className="mx_MemberTile_name">Invite</div>
-                        </div>
+                        {this.inviteTile()}
                     </div>
                 </div>
             </div>
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index e302b647..c8f2f71b 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -2,7 +2,7 @@
 function textForMemberEvent(ev) {
     // XXX: SYJS-16
     var senderName = ev.sender ? ev.sender.name : ev.getSender();
-    var targetName = ev.target ? ev.target.name : ev.getContent().state_key;
+    var targetName = ev.target ? ev.target.name : ev.getStateKey();
     var reason = ev.getContent().reason ? (
         " Reason: " + ev.getContent().reason
     ) : "";
diff --git a/src/controllers/atoms/EditableText.js b/src/controllers/atoms/EditableText.js
index 4d9eb010..15ee58a7 100644
--- a/src/controllers/atoms/EditableText.js
+++ b/src/controllers/atoms/EditableText.js
@@ -55,11 +55,16 @@ module.exports = {
         return this.state.value;
     },
 
-    setValue: function(val) {
+    setValue: function(val, shouldSubmit, suppressListener) {
+        var self = this;
         this.setState({
             value: val,
             phase: this.Phases.Display,
-        }, this.onValueChanged);
+        }, function() {
+            if (!suppressListener) {
+                self.onValueChanged(shouldSubmit);
+            }
+        });
     },
 
     edit: function() {
@@ -74,7 +79,7 @@ module.exports = {
         });
     },
 
-    onValueChanged: function() {
-        this.props.onValueChanged(this.state.value);
+    onValueChanged: function(shouldSubmit) {
+        this.props.onValueChanged(this.state.value, shouldSubmit);
     },
 };
diff --git a/src/controllers/molecules/MemberInfo.js b/src/controllers/molecules/MemberInfo.js
new file mode 100644
index 00000000..96f8bb8c
--- /dev/null
+++ b/src/controllers/molecules/MemberInfo.js
@@ -0,0 +1,115 @@
+/*
+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.
+*/
+
+/*
+ * State vars:
+ * 'presence' : string (online|offline|unavailable etc)
+ * 'active' : number (ms ago; can be -1)
+ */
+
+'use strict';
+var MatrixClientPeg = require("../../MatrixClientPeg");
+var dis = require("../../dispatcher");
+
+module.exports = {
+    componentDidMount: function() {
+        var self = this;
+        function updateUserState(event, user) {
+            if (!self.props.member) { return; }
+
+            if (user.userId === self.props.member.userId) {
+                self.setState({
+                    presence: user.presence,
+                    active: user.lastActiveAgo
+                });
+            }
+        }
+        MatrixClientPeg.get().on("User.presence", updateUserState);
+        this.userPresenceFn = updateUserState;
+
+        if (this.props.member) {
+            var usr = MatrixClientPeg.get().getUser(this.props.member.userId);
+            if (!usr) {
+                return;
+            }
+            this.setState({
+                presence: usr.presence,
+                active: usr.lastActiveAgo
+            });
+        }
+    },
+
+    componentWillUnmount: function() {
+        MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
+    },
+
+    onChatClick: function() {
+        // check if there are any existing rooms with just us and them (1:1)
+        // If so, just view that room. If not, create a private room with them.
+        var rooms = MatrixClientPeg.get().getRooms();
+        var userIds = [
+            this.props.member.userId,
+            MatrixClientPeg.get().credentials.userId
+        ];
+        var existingRoomId = null;
+        for (var i = 0; i < rooms.length; i++) {
+            var members = rooms[i].getJoinedMembers();
+            if (members.length === 2) {
+                var hasTargetUsers = true;
+                for (var j = 0; j < members.length; j++) {
+                    if (userIds.indexOf(members[j].userId) === -1) {
+                        hasTargetUsers = false;
+                        break;
+                    }
+                }
+                if (hasTargetUsers) {
+                    existingRoomId = rooms[i].roomId;
+                    break;
+                }
+            }
+        }
+
+        if (existingRoomId) {
+            dis.dispatch({
+                action: 'view_room',
+                room_id: existingRoomId
+            });
+        }
+        else {
+            MatrixClientPeg.get().createRoom({
+                invite: [this.props.member.userId],
+                preset: "private_chat"
+            }).done(function(res) {
+                dis.dispatch({
+                    action: 'view_room',
+                    room_id: res.room_id
+                });
+            }, function(err) {
+                console.error(
+                    "Failed to create room: %s", JSON.stringify(err)
+                );
+            });
+        }
+    },
+
+    getInitialState: function() {
+        return {
+            presence: "offline",
+            active: -1
+        }
+    }
+};
+
diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js
index fb5c0028..6021d0fc 100644
--- a/src/controllers/organisms/MemberList.js
+++ b/src/controllers/organisms/MemberList.js
@@ -61,6 +61,32 @@ module.exports = {
         });
     },
 
+    onInvite: function(inputText) {
+        var self = this;
+        // sanity check the input
+        inputText = inputText.trim(); // react requires es5-shim so we know trim() exists
+        if (inputText[0] !== '@' || inputText.indexOf(":") === -1) {
+            console.error("Bad user ID to invite: %s", inputText);
+            return;
+        }
+        self.setState({
+            inviting: true
+        });
+        console.log("Invite %s to %s", inputText, this.props.roomId);
+        MatrixClientPeg.get().invite(this.props.roomId, inputText).done(
+        function(res) {
+            console.log("Invited");
+            self.setState({
+                inviting: false
+            });
+        }, function(err) {
+            console.error("Failed to invite: %s", JSON.stringify(err));
+            self.setState({
+                inviting: false
+            });
+        });
+    },
+
     roomMembers: function(limit) {
         if (!this.props.roomId) return {};
         var cli = MatrixClientPeg.get();