diff --git a/src/Avatar.js b/src/Avatar.js
new file mode 100644
index 0000000000000000000000000000000000000000..02025a93849cb05c32e48d2fcaaafd768cf4fd9b
--- /dev/null
+++ b/src/Avatar.js
@@ -0,0 +1,53 @@
+/*
+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 = {
+    avatarUrlForMember: function(member, width, height, resizeMethod) {
+        var url = member.getAvatarUrl(
+            MatrixClientPeg.get().getHomeserverUrl(),
+            width,
+            height,
+            resizeMethod
+        );
+        if (!url) {
+            // member can be null here currently since on invites, the JS SDK
+            // does not have enough info to build a RoomMember object for
+            // the inviter.
+            url = this.defaultAvatarUrlForString(member ? member.userId : '');
+        }
+        return url;
+    },
+
+    defaultAvatarUrlForString: function(s) {
+        var total = 0;
+        for (var i = 0; i < s.length; ++i) {
+            total += s.charCodeAt(i);
+        }
+        switch (total % 3) {
+            case 0:
+                return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII=";
+            case 1:
+                return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9chOaxgCP4eagAFk9seHFErFYLBaLxWKxWCwWi8Vi8cX4CzAAtKMCks/JG8MAAAAASUVORK5CYII=";
+            case 2:
+                return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9YzNayQCP4eagADldseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAAyiACeHwPiu4AAAAASUVORK5CYII=";
+        }
+    }
+}
+
diff --git a/src/controllers/atoms/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js
similarity index 56%
rename from src/controllers/atoms/MemberAvatar.js
rename to src/components/views/avatars/MemberAvatar.js
index e170d2e04c71c054329b709a9e60ea86f5c5527e..f65f11256b19084b2535bb4a2b6c3ccfc5433476 100644
--- a/src/controllers/atoms/MemberAvatar.js
+++ b/src/components/views/avatars/MemberAvatar.js
@@ -17,9 +17,12 @@ limitations under the License.
 'use strict';
 
 var React = require('react');
-var MatrixClientPeg = require('../../MatrixClientPeg');
+var Avatar = require('../../../Avatar');
+var MatrixClientPeg = require('../../../MatrixClientPeg');
+
+module.exports = React.createClass({
+    displayName: 'MemberAvatar',
 
-module.exports = {
     propTypes: {
         member: React.PropTypes.object.isRequired,
         width: React.PropTypes.number,
@@ -87,5 +90,53 @@ module.exports = {
         return {
             imageUrl: this._computeUrl()
         };
+    },
+
+
+    ///////////////
+
+
+    avatarUrlForMember: function(member) {
+        return Avatar.avatarUrlForMember(
+            member,
+            this.props.member,
+            this.props.width,
+            this.props.height,
+            this.props.resizeMethod
+        );
+    },
+
+    skinnedDefaultAvatarUrl: function(member, width, height, resizeMethod) {
+        return Avatar.defaultAvatarUrlForString(member.userId);
+    },
+
+    render: function() {
+        // XXX: recalculates default avatar url constantly
+        if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
+            var initial;
+            if (this.props.member.name[0])
+                initial = this.props.member.name[0].toUpperCase();
+            if (initial === '@' && this.props.member.name[1])
+                initial = this.props.member.name[1].toUpperCase();
+         
+            return (
+                <span className="mx_MemberAvatar" {...this.props}>
+                    <span className="mx_MemberAvatar_initial" aria-hidden="true"
+                          style={{ fontSize: (this.props.width * 0.75) + "px",
+                                   width: this.props.width + "px",
+                                   lineHeight: this.props.height*1.2 + "px" }}>{ initial }</span>
+                    <img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={this.props.member.name}
+                         onError={this.onError} width={this.props.width} height={this.props.height} />
+                </span>
+            );            
+        }
+        return (
+            <img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
+                onError={this.onError}
+                width={this.props.width} height={this.props.height}
+                title={this.props.member.name}
+                {...this.props}
+            />
+        );
     }
-};
+});
diff --git a/src/controllers/atoms/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js
similarity index 65%
rename from src/controllers/atoms/RoomAvatar.js
rename to src/components/views/avatars/RoomAvatar.js
index 57c9a71842657a923d3ec7ce014b80303f9b3460..55f0e92cc1552b5781c43ee0416ac1a66f9007f9 100644
--- a/src/controllers/atoms/RoomAvatar.js
+++ b/src/components/views/avatars/RoomAvatar.js
@@ -13,18 +13,12 @@ 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');
 
-'use strict';
+module.exports = React.createClass({
+    displayName: 'RoomAvatar',
 
-var MatrixClientPeg = require('../../MatrixClientPeg');
-
-/*
- * View class should provide:
- * - getUrlList() returning an array of URLs to try for the room avatar
-     in order of preference from the most preferred at index 0. null entries
-     in the array will be skipped over.
- */
-module.exports = {
     getDefaultProps: function() {
         return {
             width: 36,
@@ -124,5 +118,58 @@ module.exports = {
         this.setState({
             imageUrl: this._nextUrl()
         });
+    },
+
+
+
+    ////////////
+
+
+    getUrlList: function() {
+        return [
+            this.roomAvatarUrl(),
+            this.getOneToOneAvatar(),
+            this.getFallbackAvatar()
+        ];
+    },
+
+    getFallbackAvatar: function() {
+        var images = [ '76cfa6', '50e2c2', 'f4c371' ];
+        var total = 0;
+        for (var i = 0; i < this.props.room.roomId.length; ++i) {
+            total += this.props.room.roomId.charCodeAt(i);
+        }
+        return 'img/' + images[total % images.length] + '.png';
+    },
+
+    render: function() {
+        var style = {
+            width: this.props.width,
+            height: this.props.height,
+        };
+
+        // XXX: recalculates fallback avatar constantly
+        if (this.state.imageUrl === this.getFallbackAvatar()) {
+            var initial;
+            if (this.props.room.name[0])
+                initial = this.props.room.name[0].toUpperCase();
+            if ((initial === '@' || initial === '#') && this.props.room.name[1])
+                initial = this.props.room.name[1].toUpperCase();
+         
+            return (
+                <span>
+                    <span className="mx_RoomAvatar_initial" aria-hidden="true"
+                          style={{ fontSize: (this.props.width * 0.75) + "px",
+                                   width: this.props.width + "px",
+                                   lineHeight: this.props.height*1.2 + "px" }}>{ initial }</span>
+                    <img className="mx_RoomAvatar" src={this.state.imageUrl}
+                            onError={this.onError} style={style} />
+                </span>
+            );
+        }
+        else {
+            return <img className="mx_RoomAvatar" src={this.state.imageUrl}
+                        onError={this.onError} style={style} />
+        }
     }
-};
+});
diff --git a/src/components/login/CaptchaForm.js b/src/components/views/login/CaptchaForm.js
similarity index 100%
rename from src/components/login/CaptchaForm.js
rename to src/components/views/login/CaptchaForm.js
diff --git a/src/components/login/CasLogin.js b/src/components/views/login/CasLogin.js
similarity index 95%
rename from src/components/login/CasLogin.js
rename to src/components/views/login/CasLogin.js
index 8a45fa0643e9fc1c75166b47c78ef520ad354322..9380db978855bfc2e089067f02f51a9943928ef4 100644
--- a/src/components/login/CasLogin.js
+++ b/src/components/views/login/CasLogin.js
@@ -16,7 +16,7 @@ limitations under the License.
 
 'use strict';
 
-var MatrixClientPeg = require("../../MatrixClientPeg");
+var MatrixClientPeg = require("../../../MatrixClientPeg");
 var React = require('react');
 var url = require("url");
 
diff --git a/src/components/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js
similarity index 100%
rename from src/components/login/PasswordLogin.js
rename to src/components/views/login/PasswordLogin.js
diff --git a/src/controllers/atoms/UserSettingsButton.js b/src/controllers/atoms/UserSettingsButton.js
deleted file mode 100644
index 5138111ef80bab9c34fd05e39541626b59ba950b..0000000000000000000000000000000000000000
--- a/src/controllers/atoms/UserSettingsButton.js
+++ /dev/null
@@ -1,27 +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_user_settings'
-        });
-    },
-};