diff --git a/src/Analytics.js b/src/Analytics.js
index 92691da1ea..a82f57a144 100644
--- a/src/Analytics.js
+++ b/src/Analytics.js
@@ -15,7 +15,6 @@
  */
 
 import { getCurrentLanguage } from './languageHandler';
-import MatrixClientPeg from './MatrixClientPeg';
 import PlatformPeg from './PlatformPeg';
 import SdkConfig from './SdkConfig';
 
@@ -31,8 +30,18 @@ const customVariables = {
     'User Type': 3,
     'Chosen Language': 4,
     'Instance': 5,
+    'RTE: Uses Richtext Mode': 6,
+    'Homeserver URL': 7,
+    'Identity Server URL': 8,
 };
 
+function whitelistRedact(whitelist, str) {
+    if (whitelist.includes(str)) return str;
+    return '<redacted>';
+}
+
+const whitelistedHSUrls = ["https://matrix.org"];
+const whitelistedISUrls = ["https://vector.im"];
 
 class Analytics {
     constructor() {
@@ -76,7 +85,7 @@ class Analytics {
         this._paq.push(['trackAllContentImpressions']);
         this._paq.push(['discardHashTag', false]);
         this._paq.push(['enableHeartBeatTimer']);
-        this._paq.push(['enableLinkTracking', true]);
+        // this._paq.push(['enableLinkTracking', true]);
 
         const platform = PlatformPeg.get();
         this._setVisitVariable('App Platform', platform.getHumanReadableName());
@@ -130,20 +139,20 @@ class Analytics {
         this._paq.push(['deleteCookies']);
     }
 
-    login() { // not used currently
-        const cli = MatrixClientPeg.get();
-        if (this.disabled || !cli) return;
-
-        this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]);
-    }
-
     _setVisitVariable(key, value) {
         this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
     }
 
-    setGuest(guest) {
+    setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
         if (this.disabled) return;
-        this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In');
+        this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
+        this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
+        this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
+    }
+
+    setRichtextMode(state) {
+        if (this.disabled) return;
+        this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
     }
 }
 
diff --git a/src/CallHandler.js b/src/CallHandler.js
index e3fbe9e5e3..8331d579df 100644
--- a/src/CallHandler.js
+++ b/src/CallHandler.js
@@ -143,7 +143,7 @@ function _setCallListeners(call) {
             pause("ringbackAudio");
             play("busyAudio");
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
                 title: _t('Call Timeout'),
                 description: _t('The remote side failed to pick up') + '.',
             });
@@ -205,7 +205,7 @@ function _onAction(payload) {
                 _setCallState(undefined, newCall.roomId, "ended");
                 console.log("Can't capture screen: " + screenCapErrorString);
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
                     title: _t('Unable to capture screen'),
                     description: screenCapErrorString,
                 });
@@ -225,7 +225,7 @@ function _onAction(payload) {
         case 'place_call':
             if (module.exports.getAnyActiveCall()) {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
                     title: _t('Existing Call'),
                     description: _t('You are already in a call.'),
                 });
@@ -235,7 +235,7 @@ function _onAction(payload) {
             // if the runtime env doesn't do VoIP, whine.
             if (!MatrixClientPeg.get().supportsVoip()) {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
                     title: _t('VoIP is unsupported'),
                     description: _t('You cannot place VoIP calls in this browser.'),
                 });
@@ -251,7 +251,7 @@ function _onAction(payload) {
             var members = room.getJoinedMembers();
             if (members.length <= 1) {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
                     description: _t('You cannot place a call with yourself.'),
                 });
                 return;
@@ -277,13 +277,13 @@ function _onAction(payload) {
             console.log("Place conference call in %s", payload.room_id);
             if (!ConferenceHandler) {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
                     description: _t('Conference calls are not supported in this client'),
                 });
             }
             else if (!MatrixClientPeg.get().supportsVoip()) {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
                     title: _t('VoIP is unsupported'),
                     description: _t('You cannot place VoIP calls in this browser.'),
                 });
@@ -296,13 +296,13 @@ function _onAction(payload) {
                 // participant.
                 // Therefore we disable conference calling in E2E rooms.
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
                     description: _t('Conference calls are not supported in encrypted rooms'),
                 });
             }
             else {
                 var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-                Modal.createDialog(QuestionDialog, {
+                Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
                     title: _t('Warning!'),
                     description: _t('Conference calling is in development and may not be reliable.'),
                     onFinished: confirm=>{
@@ -314,7 +314,7 @@ function _onAction(payload) {
                             }, function(err) {
                                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                                 console.error("Conference call failed: " + err);
-                                Modal.createDialog(ErrorDialog, {
+                                Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, {
                                     title: _t('Failed to set up conference call'),
                                     description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
                                 });
diff --git a/src/ContentMessages.js b/src/ContentMessages.js
index 9239de9d8f..93057fafed 100644
--- a/src/ContentMessages.js
+++ b/src/ContentMessages.js
@@ -360,7 +360,7 @@ class ContentMessages {
                     desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
                 }
                 var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
                     title: _t('Upload Failed'),
                     description: desc,
                 });
diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js
index 1da4922153..0b54d88e5f 100644
--- a/src/KeyRequestHandler.js
+++ b/src/KeyRequestHandler.js
@@ -125,7 +125,7 @@ export default class KeyRequestHandler {
         };
 
         const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
-        Modal.createDialog(KeyShareDialog, {
+        Modal.createTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, {
             matrixClient: this._matrixClient,
             userId: userId,
             deviceId: deviceId,
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index eb2156e780..4d8911f7a6 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -240,7 +240,7 @@ function _handleRestoreFailure(e) {
     const SessionRestoreErrorDialog =
           sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
 
-    Modal.createDialog(SessionRestoreErrorDialog, {
+    Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
         error: e.message,
         onFinished: (success) => {
             def.resolve(success);
@@ -318,7 +318,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
         await _clearStorage();
     }
 
-    Analytics.setGuest(credentials.guest);
+    Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl);
 
     // Resolves by default
     let teamPromise = Promise.resolve(null);
diff --git a/src/Modal.js b/src/Modal.js
index e100105a88..79fcaaefd1 100644
--- a/src/Modal.js
+++ b/src/Modal.js
@@ -103,13 +103,20 @@ class ModalManager {
         return container;
     }
 
+    createTrackedDialog(analyticsAction, analyticsInfo, Element, props, className) {
+        Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
+        return this.createDialog(Element, props, className);
+    }
+
     createDialog(Element, props, className) {
-        if (props && props.title) {
-            Analytics.trackEvent('Modal', props.title, 'createDialog');
-        }
         return this.createDialogAsync((cb) => {cb(Element);}, props, className);
     }
 
+    createTrackedDialogAsync(analyticsId, loader, props, className) {
+        Analytics.trackEvent('Modal', analyticsId);
+        return this.createDialogAsync(loader, props, className);
+    }
+
     /**
      * Open a modal view.
      *
diff --git a/src/Notifier.js b/src/Notifier.js
index 40a65d4106..1bb435307d 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -142,7 +142,7 @@ const Notifier = {
                         ? _t('Riot does not have permission to send you notifications - please check your browser settings')
                         : _t('Riot was not given permission to send notifications - please try again');
                     const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
-                    Modal.createDialog(ErrorDialog, {
+                    Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, {
                         title: _t('Unable to enable Notifications'),
                         description,
                     });
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index dea3d27751..e5378d4347 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -68,7 +68,7 @@ const commands = {
     ddg: new Command("ddg", "<query>", function(roomId, args) {
         const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
         // TODO Don't explain this away, actually show a search UI here.
-        Modal.createDialog(ErrorDialog, {
+        Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
             title: _t('/ddg is not a command'),
             description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
         });
@@ -326,13 +326,11 @@ const commands = {
                                    {deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
                         }
 
-                        return MatrixClientPeg.get().setDeviceVerified(
-                            userId, deviceId, true,
-                        );
+                        return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true);
                     }).then(() => {
                         // Tell the user we verified everything
                         const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-                        Modal.createDialog(QuestionDialog, {
+                        Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
                             title: _t("Verified key"),
                             description: (
                                 <div>
diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js
index 2b1cf23380..e7d77b3b66 100644
--- a/src/UnknownDeviceErrorHandler.js
+++ b/src/UnknownDeviceErrorHandler.js
@@ -24,7 +24,7 @@ const onAction = function(payload) {
     if (payload.action === 'unknown_device_error' && !isDialogOpen) {
         const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
         isDialogOpen = true;
-        Modal.createDialog(UnknownDeviceDialog, {
+        Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
             devices: payload.err.devices,
             room: payload.room,
             onFinished: (r) => {
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 5f7866773d..20fc4841ba 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -266,7 +266,7 @@ export default React.createClass({
             this.setState({uploadingAvatar: false});
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Failed to upload avatar image", e);
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to upload image', '', ErrorDialog, {
                 title: _t('Error'),
                 description: _t('Failed to upload image'),
             });
@@ -288,7 +288,7 @@ export default React.createClass({
             });
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Failed to save group profile", e);
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to update group', '', ErrorDialog, {
                 title: _t('Error'),
                 description: _t('Failed to update group'),
             });
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index cb6419c9e8..6fdec80f38 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -410,7 +410,7 @@ module.exports = React.createClass({
                 this._leaveRoom(payload.room_id);
                 break;
             case 'reject_invite':
-                Modal.createDialog(QuestionDialog, {
+                Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
                     title: _t('Reject invitation'),
                     description: _t('Are you sure you want to reject the invitation?'),
                     onFinished: (confirm) => {
@@ -426,7 +426,7 @@ module.exports = React.createClass({
                                 }
                             }, (err) => {
                                 modal.close();
-                                Modal.createDialog(ErrorDialog, {
+                                Modal.createTrackedDialog('Failed to reject invitation', '', ErrorDialog, {
                                     title: _t('Failed to reject invitation'),
                                     description: err.toString(),
                                 });
@@ -728,7 +728,7 @@ module.exports = React.createClass({
 
     _setMxId: function(payload) {
         const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
-        const close = Modal.createDialog(SetMxIdDialog, {
+        const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
             homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
             onFinished: (submitted, credentials) => {
                 if (!submitted) {
@@ -767,7 +767,7 @@ module.exports = React.createClass({
             return;
         }
         const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
-        Modal.createDialog(ChatInviteDialog, {
+        Modal.createTrackedDialog('Start a chat', '', ChatInviteDialog, {
             title: _t('Start a chat'),
             description: _t("Who would you like to communicate with?"),
             placeholder: _t("Email, name or matrix ID"),
@@ -787,7 +787,7 @@ module.exports = React.createClass({
             return;
         }
         const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
-        Modal.createDialog(TextInputDialog, {
+        Modal.createTrackedDialog('Create Room', '', TextInputDialog, {
             title: _t('Create Room'),
             description: _t('Room name (optional)'),
             button: _t('Create Room'),
@@ -831,7 +831,7 @@ module.exports = React.createClass({
             return;
         }
 
-        const close = Modal.createDialog(ChatCreateOrReuseDialog, {
+        const close = Modal.createTrackedDialog('Chat create or reuse', '', ChatCreateOrReuseDialog, {
             userId: userId,
             onFinished: (success) => {
                 if (!success && goHomeOnCancel) {
@@ -859,7 +859,7 @@ module.exports = React.createClass({
 
     _invite: function(roomId) {
         const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
-        Modal.createDialog(ChatInviteDialog, {
+        Modal.createTrackedDialog('Chat Invite', '', ChatInviteDialog, {
             title: _t('Invite new room members'),
             description: _t('Who would you like to add to this room?'),
             button: _t('Send Invites'),
@@ -873,7 +873,7 @@ module.exports = React.createClass({
         const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 
         const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
-        Modal.createDialog(QuestionDialog, {
+        Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
             title: _t("Leave room"),
             description: (
                 <span>
@@ -896,7 +896,7 @@ module.exports = React.createClass({
                     }, (err) => {
                         modal.close();
                         console.error("Failed to leave room " + roomId + " " + err);
-                        Modal.createDialog(ErrorDialog, {
+                        Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
                             title: _t("Failed to leave room"),
                             description: (err && err.message ? err.message :
                                 _t("Server may be unavailable, overloaded, or you hit a bug.")),
@@ -1090,7 +1090,7 @@ module.exports = React.createClass({
         });
         cli.on('Session.logged_out', function(call) {
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Signed out', '', ErrorDialog, {
                 title: _t('Signed Out'),
                 description: _t('For security, this session has been signed out. Please sign in again.'),
             });
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index 3eb694acce..0b8055beda 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -63,7 +63,7 @@ export default withMatrixClient(React.createClass({
 
     _onCreateGroupClick: function() {
         const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
-        Modal.createDialog(CreateGroupDialog);
+        Modal.createTrackedDialog('Create Group', '', CreateGroupDialog);
     },
 
     _fetch: function() {
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 094251f4c1..f825d1efbb 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -544,7 +544,7 @@ module.exports = React.createClass({
         }
         if (!userHasUsedEncryption) {
             const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-            Modal.createDialog(QuestionDialog, {
+            Modal.createTrackedDialog('E2E Warning', '', QuestionDialog, {
                 title: _t("Warning!"),
                 hasCancelButton: false,
                 description: (
@@ -820,7 +820,7 @@ module.exports = React.createClass({
             });
 
             const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
-            const close = Modal.createDialog(SetMxIdDialog, {
+            const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
                 homeserverUrl: cli.getHomeserverUrl(),
                 onFinished: (submitted, credentials) => {
                     if (submitted) {
@@ -934,7 +934,7 @@ module.exports = React.createClass({
             }
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Failed to upload file " + file + " " + error);
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
                 title: _t('Failed to upload file'),
                 description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
             });
@@ -1021,7 +1021,7 @@ module.exports = React.createClass({
         }, function(error) {
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Search failed: " + error);
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
                 title: _t("Search failed"),
                 description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
             });
@@ -1148,7 +1148,7 @@ module.exports = React.createClass({
                     console.error(result.reason);
                 });
                 var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Failed to save room settings', '', ErrorDialog, {
                     title: _t("Failed to save settings"),
                     description: fails.map(function(result) { return result.reason; }).join("\n"),
                 });
@@ -1195,7 +1195,7 @@ module.exports = React.createClass({
         }, function(err) {
             var errCode = err.errcode || _t("unknown error code");
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
                 title: _t("Error"),
                 description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
             });
@@ -1217,7 +1217,7 @@ module.exports = React.createClass({
 
             var msg = error.message ? error.message : JSON.stringify(error);
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
                 title: _t("Failed to reject invite"),
                 description: msg,
             });
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 0aee19545c..6be31361dd 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -923,7 +923,7 @@ var TimelinePanel = React.createClass({
             var message = (error.errcode == 'M_FORBIDDEN')
             	? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
                 : _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
                 title: _t("Failed to load timeline position"),
                 description: message,
                 onFinished: onFinished,
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 916e50d86b..e2463a3ac7 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -85,6 +85,10 @@ const SETTINGS_LABELS = [
         id: 'hideJoinLeaves',
         label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
     },
+    {
+        id: 'hideAvatarDisplaynameChanges',
+        label: 'Hide avatar and display name changes',
+    },
     {
         id: 'useCompactLayout',
         label: 'Use compact timeline layout',
@@ -335,7 +339,7 @@ module.exports = React.createClass({
         }, function(error) {
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Failed to load user settings: " + error);
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Can\'t load user settings', '', ErrorDialog, {
                 title: _t("Can't load user settings"),
                 description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
             });
@@ -368,7 +372,7 @@ module.exports = React.createClass({
             // const errMsg = (typeof err === "string") ? err : (err.error || "");
             console.error("Failed to set avatar: " + err);
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, {
                 title: _t("Failed to set avatar."),
                 description: ((err && err.message) ? err.message : _t("Operation failed")),
             });
@@ -377,7 +381,7 @@ module.exports = React.createClass({
 
     onLogoutClicked: function(ev) {
         const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-        Modal.createDialog(QuestionDialog, {
+        Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
             title: _t("Sign out"),
             description:
                 <div>
@@ -413,7 +417,7 @@ module.exports = React.createClass({
         }
         const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
         console.error("Failed to change password: " + errMsg);
-        Modal.createDialog(ErrorDialog, {
+        Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
             title: _t("Error"),
             description: errMsg,
         });
@@ -421,7 +425,7 @@ module.exports = React.createClass({
 
     onPasswordChanged: function() {
         const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-        Modal.createDialog(ErrorDialog, {
+        Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
             title: _t("Success"),
             description: _t(
                 "Your password was successfully changed. You will not receive " +
@@ -446,7 +450,7 @@ module.exports = React.createClass({
 
         const emailAddress = this.refs.add_email_input.value;
         if (!Email.looksValid(emailAddress)) {
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, {
                 title: _t("Invalid Email Address"),
                 description: _t("This doesn't appear to be a valid email address"),
             });
@@ -456,7 +460,7 @@ module.exports = React.createClass({
         // we always bind emails when registering, so let's do the
         // same here.
         this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
-            Modal.createDialog(QuestionDialog, {
+            Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
                 title: _t("Verification Pending"),
                 description: _t(
                     "Please check your email and click on the link it contains. Once this " +
@@ -468,7 +472,7 @@ module.exports = React.createClass({
         }, (err) => {
             this.setState({email_add_pending: false});
             console.error("Unable to add email address " + emailAddress + " " + err);
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
                 title: _t("Unable to add email address"),
                 description: ((err && err.message) ? err.message : _t("Operation failed")),
             });
@@ -479,7 +483,7 @@ module.exports = React.createClass({
 
     onRemoveThreepidClicked: function(threepid) {
         const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-        Modal.createDialog(QuestionDialog, {
+        Modal.createTrackedDialog('Remove 3pid', '', QuestionDialog, {
             title: _t("Remove Contact Information?"),
             description: _t("Remove %(threePid)s?", { threePid: threepid.address }),
             button: _t('Remove'),
@@ -493,7 +497,7 @@ module.exports = React.createClass({
                     }).catch((err) => {
                         const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                         console.error("Unable to remove contact information: " + err);
-                        Modal.createDialog(ErrorDialog, {
+                        Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
                             title: _t("Unable to remove contact information"),
                             description: ((err && err.message) ? err.message : _t("Operation failed")),
                         });
@@ -525,7 +529,7 @@ module.exports = React.createClass({
                 const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
                 const message = _t("Unable to verify email address.") + " " +
                     _t("Please check your email and click on the link it contains. Once this is done, click continue.");
-                Modal.createDialog(QuestionDialog, {
+                Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
                     title: _t("Verification Pending"),
                     description: message,
                     button: _t('Continue'),
@@ -534,7 +538,7 @@ module.exports = React.createClass({
             } else {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                 console.error("Unable to verify email address: " + err);
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
                     title: _t("Unable to verify email address."),
                     description: ((err && err.message) ? err.message : _t("Operation failed")),
                 });
@@ -544,7 +548,7 @@ module.exports = React.createClass({
 
     _onDeactivateAccountClicked: function() {
         const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog");
-        Modal.createDialog(DeactivateAccountDialog, {});
+        Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {});
     },
 
     _onBugReportClicked: function() {
@@ -552,7 +556,7 @@ module.exports = React.createClass({
         if (!BugReportDialog) {
             return;
         }
-        Modal.createDialog(BugReportDialog, {});
+        Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
     },
 
     _onClearCacheClicked: function() {
@@ -589,27 +593,23 @@ module.exports = React.createClass({
     },
 
     _onExportE2eKeysClicked: function() {
-        Modal.createDialogAsync(
-            (cb) => {
-                require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
-                    cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
-                }, "e2e-export");
-            }, {
-                matrixClient: MatrixClientPeg.get(),
-            },
-        );
+        Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => {
+            require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
+                cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
+            }, "e2e-export");
+        }, {
+            matrixClient: MatrixClientPeg.get(),
+        });
     },
 
     _onImportE2eKeysClicked: function() {
-        Modal.createDialogAsync(
-            (cb) => {
-                require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
-                    cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
-                }, "e2e-export");
-            }, {
-                matrixClient: MatrixClientPeg.get(),
-            },
-        );
+        Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => {
+            require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
+                cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
+            }, "e2e-export");
+        }, {
+            matrixClient: MatrixClientPeg.get(),
+        });
     },
 
     _renderReferral: function() {
@@ -1008,7 +1008,7 @@ module.exports = React.createClass({
                 this._refreshMediaDevices,
                 function() {
                     const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
-                    Modal.createDialog(ErrorDialog, {
+                    Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
                         title: _t('No media permissions'),
                         description: _t('You may need to manually permit Riot to access your microphone/webcam'),
                     });
diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js
index 18a9dca5dd..320d21f5b4 100644
--- a/src/components/structures/login/ForgotPassword.js
+++ b/src/components/structures/login/ForgotPassword.js
@@ -89,14 +89,14 @@ module.exports = React.createClass({
         }
         else {
             var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-            Modal.createDialog(QuestionDialog, {
+            Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
                 title: _t('Warning!'),
                 description:
                     <div>
                         { _t(
                             'Resetting password will currently reset any ' +
                             'end-to-end encryption keys on all devices, ' +
-                            'making encrypted chat history unreadable, ' + 
+                            'making encrypted chat history unreadable, ' +
                             'unless you first export your room keys and re-import ' +
                             'them afterwards. In future this will be improved.'
                         ) }
@@ -121,15 +121,13 @@ module.exports = React.createClass({
     },
 
     _onExportE2eKeysClicked: function() {
-        Modal.createDialogAsync(
-            (cb) => {
-                require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
-                    cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
-                }, "e2e-export");
-            }, {
-                matrixClient: MatrixClientPeg.get(),
-            }
-        );
+        Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', (cb) => {
+            require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
+                cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
+            }, "e2e-export");
+        }, {
+            matrixClient: MatrixClientPeg.get(),
+        });
     },
 
     onInputChanged: function(stateKey, ev) {
@@ -152,7 +150,7 @@ module.exports = React.createClass({
 
     showErrorDialog: function(body, title) {
         var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-        Modal.createDialog(ErrorDialog, {
+        Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
             title: title,
             description: body,
         });
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index a081d2a205..a6c0a70c66 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -19,8 +19,11 @@ limitations under the License.
 
 import React from 'react';
 import { _t, _tJsx } from '../../../languageHandler';
+import * as languageHandler from '../../../languageHandler';
 import sdk from '../../../index';
 import Login from '../../../Login';
+import UserSettingsStore from '../../../UserSettingsStore';
+import PlatformPeg from '../../../PlatformPeg';
 
 // For validating phone numbers without country codes
 const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
@@ -306,6 +309,23 @@ module.exports = React.createClass({
         }
     },
 
+    _onLanguageChange: function(newLang) {
+        if(languageHandler.getCurrentLanguage() !== newLang) {
+            UserSettingsStore.setLocalSetting('language', newLang);
+            PlatformPeg.get().reload();
+        }
+    },
+
+    _renderLanguageSetting: function() {
+        const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
+        return <div className="mx_Login_language_div">
+            <LanguageDropdown onOptionChange={this._onLanguageChange}
+                          className="mx_Login_language"
+                          value={languageHandler.getCurrentLanguage()}
+            />
+        </div>;
+    },
+
     render: function() {
         const Loader = sdk.getComponent("elements.Spinner");
         const LoginHeader = sdk.getComponent("login.LoginHeader");
@@ -354,6 +374,7 @@ module.exports = React.createClass({
                         </a>
                         { loginAsGuestJsx }
                         { returnToAppJsx }
+                        { this._renderLanguageSetting() }
                         <LoginFooter />
                     </div>
                 </div>
diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js
index d3a208a785..728860edec 100644
--- a/src/components/views/dialogs/ChatInviteDialog.js
+++ b/src/components/views/dialogs/ChatInviteDialog.js
@@ -103,7 +103,7 @@ module.exports = React.createClass({
                     const ChatCreateOrReuseDialog = sdk.getComponent(
                         "views.dialogs.ChatCreateOrReuseDialog",
                     );
-                    const close = Modal.createDialog(ChatCreateOrReuseDialog, {
+                    const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
                         userId: userId,
                         onFinished: (success) => {
                             this.props.onFinished(success);
@@ -367,7 +367,7 @@ module.exports = React.createClass({
             .catch(function(err) {
                 console.error(err.stack);
                 var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
                     title: _t("Failed to invite"),
                     description: ((err && err.message) ? err.message : _t("Operation failed")),
                 });
@@ -380,7 +380,7 @@ module.exports = React.createClass({
             .catch(function(err) {
                 console.error(err.stack);
                 var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
                     title: _t("Failed to invite user"),
                     description: ((err && err.message) ? err.message : _t("Operation failed")),
                 });
@@ -401,7 +401,7 @@ module.exports = React.createClass({
             .catch(function(err) {
                 console.error(err.stack);
                 var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
                     title: _t("Failed to invite"),
                     description: ((err && err.message) ? err.message : _t("Operation failed")),
                 });
@@ -448,7 +448,7 @@ module.exports = React.createClass({
 
         if (errorList.length > 0) {
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
                 title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
                 description: errorList.join(", "),
             });
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js
index e3b7cca078..0ee264b69b 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.js
+++ b/src/components/views/dialogs/DeactivateAccountDialog.js
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 
 import sdk from '../../../index';
+import Analytics from '../../../Analytics';
 import MatrixClientPeg from '../../../MatrixClientPeg';
 import * as Lifecycle from '../../../Lifecycle';
 import Velocity from 'velocity-vector';
@@ -54,6 +55,7 @@ export default class DeactivateAccountDialog extends React.Component {
             user: MatrixClientPeg.get().credentials.userId,
             password: this._passwordField.value,
         }).done(() => {
+            Analytics.trackEvent('Account', 'Deactivate Account');
             Lifecycle.onLoggedOut();
             this.props.onFinished(false);
         }, (err) => {
diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js
index bf48d1757b..beca107252 100644
--- a/src/components/views/dialogs/ErrorDialog.js
+++ b/src/components/views/dialogs/ErrorDialog.js
@@ -16,7 +16,7 @@ limitations under the License.
 
 /*
  * Usage:
- * Modal.createDialog(ErrorDialog, {
+ * Modal.createTrackedDialog('An Identifier', 'some detail', ErrorDialog, {
  *   title: "some text", (default: "Error")
  *   description: "some more text",
  *   button: "Button Text",
diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js
index 61391d281c..aed8e6a5af 100644
--- a/src/components/views/dialogs/KeyShareDialog.js
+++ b/src/components/views/dialogs/KeyShareDialog.js
@@ -88,7 +88,7 @@ export default React.createClass({
         const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
 
         console.log("KeyShareDialog: Starting verify dialog");
-        Modal.createDialog(DeviceVerifyDialog, {
+        Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
             userId: this.props.userId,
             device: this.state.deviceInfo,
             onFinished: (verified) => {
diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js
index a3eb7c6962..010072e8c6 100644
--- a/src/components/views/dialogs/SessionRestoreErrorDialog.js
+++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js
@@ -31,7 +31,7 @@ export default React.createClass({
 
     _sendBugReport: function() {
         const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
-        Modal.createDialog(BugReportDialog, {});
+        Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
     },
 
     _continueClicked: function() {
diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js
index 3c38064ee1..ed5cef2f67 100644
--- a/src/components/views/dialogs/SetEmailDialog.js
+++ b/src/components/views/dialogs/SetEmailDialog.js
@@ -55,7 +55,7 @@ export default React.createClass({
 
         const emailAddress = this.state.emailAddress;
         if (!Email.looksValid(emailAddress)) {
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, {
                 title: _t("Invalid Email Address"),
                 description: _t("This doesn't appear to be a valid email address"),
             });
@@ -65,7 +65,7 @@ export default React.createClass({
         // we always bind emails when registering, so let's do the
         // same here.
         this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
-            Modal.createDialog(QuestionDialog, {
+            Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
                 title: _t("Verification Pending"),
                 description: _t(
                     "Please check your email and click on the link it contains. Once this " +
@@ -77,7 +77,7 @@ export default React.createClass({
         }, (err) => {
             this.setState({emailBusy: false});
             console.error("Unable to add email address " + emailAddress + " " + err);
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
                 title: _t("Unable to add email address"),
                 description: ((err && err.message) ? err.message : _t("Operation failed")),
             });
@@ -106,7 +106,7 @@ export default React.createClass({
                 const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
                 const message = _t("Unable to verify email address.") + " " +
                     _t("Please check your email and click on the link it contains. Once this is done, click continue.");
-                Modal.createDialog(QuestionDialog, {
+                Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, {
                     title: _t("Verification Pending"),
                     description: message,
                     button: _t('Continue'),
@@ -115,7 +115,7 @@ export default React.createClass({
             } else {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                 console.error("Unable to verify email address: " + err);
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
                     title: _t("Unable to verify email address."),
                     description: ((err && err.message) ? err.message : _t("Operation failed")),
                 });
diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js
index 4d4f672f2b..554a244358 100644
--- a/src/components/views/dialogs/SetMxIdDialog.js
+++ b/src/components/views/dialogs/SetMxIdDialog.js
@@ -106,6 +106,16 @@ export default React.createClass({
     },
 
     _doUsernameCheck: function() {
+        // XXX: SPEC-1
+        // Check if username is valid
+        // Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190
+        if (encodeURIComponent(this.state.username) !== this.state.username) {
+            this.setState({
+                usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'),
+            });
+            return Promise.reject();
+        }
+
         // Check if username is available
         return this._matrixClient.isUsernameAvailable(this.state.username).then(
             (isAvailable) => {
@@ -242,7 +252,7 @@ export default React.createClass({
         return (
             <BaseDialog className="mx_SetMxIdDialog"
                 onFinished={this.props.onFinished}
-                title="To get started, please pick a username!"
+                title={_t('To get started, please pick a username!')}
             >
                 <div className="mx_Dialog_content">
                     <div className="mx_SetMxIdDialog_input_group">
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 2fcacaaee2..a78b802ad7 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -123,7 +123,7 @@ export default React.createClass({
         console.log("Edit widget ID ", this.props.id);
         const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
         const src = this._scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'type_' + this.props.type);
-        Modal.createDialog(IntegrationsManager, {
+        Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
             src: src,
         }, "mx_IntegrationsManager");
     },
diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js
index dfca7e2600..bfe45905a1 100644
--- a/src/components/views/elements/DeviceVerifyButtons.js
+++ b/src/components/views/elements/DeviceVerifyButtons.js
@@ -52,7 +52,7 @@ export default React.createClass({
 
     onVerifyClick: function() {
         const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
-        Modal.createDialog(DeviceVerifyDialog, {
+        Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
             userId: this.props.userId,
             device: this.state.device,
         });
diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js
index ff07cd36e5..d5b7bcf46a 100644
--- a/src/components/views/login/RegistrationForm.js
+++ b/src/components/views/login/RegistrationForm.js
@@ -95,7 +95,7 @@ module.exports = React.createClass({
         if (this.allFieldsValid()) {
             if (this.refs.email.value == '') {
                 var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-                Modal.createDialog(QuestionDialog, {
+                Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
                     title: _t("Warning!"),
                     description:
                         <div>
diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js
index a63d02416c..0042ab5e9f 100644
--- a/src/components/views/login/ServerConfig.js
+++ b/src/components/views/login/ServerConfig.js
@@ -122,7 +122,7 @@ module.exports = React.createClass({
 
     showHelpPopup: function() {
         var CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
-        Modal.createDialog(CustomServerDialog);
+        Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
     },
 
     render: function() {
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index bccae923eb..53c36f234c 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -282,8 +282,8 @@ module.exports = React.createClass({
                         });
                     }).catch((err) => {
                         console.warn("Unable to decrypt attachment: ", err);
-                        Modal.createDialog(ErrorDialog, {
-                        	title: _t("Error"),
+                        Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
+                            title: _t("Error"),
                             description: _t("Error decrypting attachment"),
                         });
                     }).finally(() => {
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 27dba76146..58273bee67 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -275,18 +275,21 @@ module.exports = React.createClass({
     },
 
     getEventTileOps: function() {
-        var self = this;
         return {
-            isWidgetHidden: function() {
-                return self.state.widgetHidden;
+            isWidgetHidden: () => {
+                return this.state.widgetHidden;
             },
 
-            unhideWidget: function() {
-                self.setState({ widgetHidden: false });
+            unhideWidget: () => {
+                this.setState({ widgetHidden: false });
                 if (global.localStorage) {
-                    global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId());
+                    global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId());
                 }
             },
+
+            getInnerText: () => {
+                return this.refs.content.innerText;
+            }
         };
     },
 
@@ -305,7 +308,7 @@ module.exports = React.createClass({
             let completeUrl = scalarClient.getStarterLink(starterLink);
             let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
             let integrationsUrl = SdkConfig.get().integrations_ui_url;
-            Modal.createDialog(QuestionDialog, {
+            Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
                 title: _t("Add an Integration"),
                 description:
                     <div>
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js
index ba0663153e..f37bd4271a 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.js
@@ -154,7 +154,7 @@ module.exports = React.createClass({
         }
         else {
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
                 title: _t('Invalid alias format'),
                 description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
             });
@@ -170,7 +170,7 @@ module.exports = React.createClass({
         }
         else {
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
                 title: _t('Invalid address format'),
                 description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
             });
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index 5427d4ec6d..4bc98abb6f 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -166,7 +166,7 @@ module.exports = React.createClass({
         const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
                 this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
                 null;
-        Modal.createDialog(IntegrationsManager, {
+        Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
             src: src,
         }, "mx_IntegrationsManager");
     },
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 815f0a3c6a..776d8a264b 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -369,7 +369,7 @@ module.exports = withMatrixClient(React.createClass({
     onCryptoClicked: function(e) {
         var event = this.props.mxEvent;
 
-        Modal.createDialogAsync((cb) => {
+        Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', (cb) => {
             require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb);
         }, {
             event: event,
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 290bd35483..64eeddb406 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -229,7 +229,7 @@ module.exports = withMatrixClient(React.createClass({
         const membership = this.props.member.membership;
         const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
         const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
-        Modal.createDialog(ConfirmUserActionDialog, {
+        Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, {
             member: this.props.member,
             action: kickLabel,
             askReason: membership == "join",
@@ -248,7 +248,7 @@ module.exports = withMatrixClient(React.createClass({
                     }, function(err) {
                         const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                         console.error("Kick error: " + err);
-                        Modal.createDialog(ErrorDialog, {
+                        Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, {
                             title: _t("Failed to kick"),
                             description: ((err && err.message) ? err.message : "Operation failed"),
                         });
@@ -262,7 +262,7 @@ module.exports = withMatrixClient(React.createClass({
 
     onBanOrUnban: function() {
         const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
-        Modal.createDialog(ConfirmUserActionDialog, {
+        Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, {
             member: this.props.member,
             action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"),
             askReason: this.props.member.membership != 'ban',
@@ -290,7 +290,7 @@ module.exports = withMatrixClient(React.createClass({
                     }, function(err) {
                         const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                         console.error("Ban error: " + err);
-                        Modal.createDialog(ErrorDialog, {
+                        Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, {
                             title: _t("Error"),
                             description: _t("Failed to ban user"),
                         });
@@ -340,7 +340,7 @@ module.exports = withMatrixClient(React.createClass({
                     console.log("Mute toggle success");
                 }, function(err) {
                     console.error("Mute error: " + err);
-                    Modal.createDialog(ErrorDialog, {
+                    Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
                         title: _t("Error"),
                         description: _t("Failed to mute user"),
                     });
@@ -385,7 +385,7 @@ module.exports = withMatrixClient(React.createClass({
                     dis.dispatch({action: 'view_set_mxid'});
                 } else {
                     console.error("Toggle moderator error:" + err);
-                    Modal.createDialog(ErrorDialog, {
+                    Modal.createTrackedDialog('Failed to toggle moderator status', '', ErrorDialog, {
                         title: _t("Error"),
                         description: _t("Failed to toggle moderator status"),
                     });
@@ -406,7 +406,7 @@ module.exports = withMatrixClient(React.createClass({
             }, function(err) {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                 console.error("Failed to change power level " + err);
-                Modal.createDialog(ErrorDialog, {
+                Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
                     title: _t("Error"),
                     description: _t("Failed to change power level"),
                 });
@@ -435,7 +435,7 @@ module.exports = withMatrixClient(React.createClass({
             var myPower = powerLevelEvent.getContent().users[this.props.matrixClient.credentials.userId];
             if (parseInt(myPower) === parseInt(powerLevel)) {
                 var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-                Modal.createDialog(QuestionDialog, {
+                Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
                     title: _t("Warning!"),
                     description:
                         <div>
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 7c8723e197..fd8c73eb80 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -99,7 +99,7 @@ export default class MessageComposer extends React.Component {
             </li>);
         }
 
-        Modal.createDialog(QuestionDialog, {
+        Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
             title: _t('Upload Files'),
             description: (
                 <div>
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index b2c1436365..63be026608 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -31,6 +31,7 @@ import KeyCode from '../../../KeyCode';
 import Modal from '../../../Modal';
 import sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import Analytics from '../../../Analytics';
 
 import dis from '../../../dispatcher';
 import UserSettingsStore from '../../../UserSettingsStore';
@@ -160,6 +161,8 @@ export default class MessageComposerInput extends React.Component {
 
         const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
 
+        Analytics.setRichtextMode(isRichtextEnabled);
+
         this.state = {
             // whether we're in rich text or markdown mode
             isRichtextEnabled,
@@ -280,11 +283,10 @@ export default class MessageComposerInput extends React.Component {
             }
                 break;
             case 'quote': {
-                let {body} = payload.event.getContent();
                 /// XXX: Not doing rich-text quoting from formatted-body because draft-js
                 /// has regressed such that when links are quoted, errors are thrown. See
                 /// https://github.com/vector-im/riot-web/issues/4756.
-                body = escape(body);
+                let body = escape(payload.text);
                 if (body) {
                     let content = RichText.htmlToContentState(`<blockquote>${body}</blockquote>`);
                     if (!this.state.isRichtextEnabled) {
@@ -457,6 +459,19 @@ export default class MessageComposerInput extends React.Component {
             state.editorState = RichText.attachImmutableEntitiesToEmoji(
                 state.editorState);
 
+            // Hide the autocomplete if the cursor location changes but the plaintext
+            // content stays the same. We don't hide if the pt has changed because the
+            // autocomplete will probably have different completions to show.
+            if (
+                !state.editorState.getSelection().equals(
+                    this.state.editorState.getSelection()
+                )
+                && state.editorState.getCurrentContent().getPlainText() ===
+                this.state.editorState.getCurrentContent().getPlainText()
+            ) {
+                this.autocomplete.hide();
+            }
+
             if (state.editorState.getCurrentContent().hasText()) {
                 this.onTypingActivity();
             } else {
@@ -513,6 +528,8 @@ export default class MessageComposerInput extends React.Component {
             contentState = ContentState.createFromText(markdown);
         }
 
+        Analytics.setRichtextMode(enabled);
+
         this.setState({
             editorState: this.createEditorState(enabled, contentState),
             isRichtextEnabled: enabled,
@@ -704,7 +721,7 @@ export default class MessageComposerInput extends React.Component {
                 }, function(err) {
                     console.error("Command failure: %s", err);
                     const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                    Modal.createDialog(ErrorDialog, {
+                    Modal.createTrackedDialog('Server error', '', ErrorDialog, {
                         title: _t("Server error"),
                         description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
                     });
@@ -712,7 +729,8 @@ export default class MessageComposerInput extends React.Component {
             } else if (cmd.error) {
                 console.error(cmd.error);
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                Modal.createDialog(ErrorDialog, {
+                // TODO possibly track which command they ran (not its Arguments) here
+                Modal.createTrackedDialog('Command error', '', ErrorDialog, {
                     title: _t("Command error"),
                     description: cmd.error,
                 });
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index ed354017b2..319ce92f4f 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -122,7 +122,7 @@ module.exports = React.createClass({
             const errMsg = (typeof err === "string") ? err : (err.error || "");
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Failed to set avatar: " + errMsg);
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, {
                 title: _t("Error"),
                 description: _t("Failed to set avatar."),
             });
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index 0c1372f54e..8cc7c48676 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -44,7 +44,7 @@ const BannedUser = React.createClass({
 
     _onUnbanClick: function() {
         const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
-        Modal.createDialog(ConfirmUserActionDialog, {
+        Modal.createTrackedDialog('Confirm User Action Dialog', 'onUnbanClick', ConfirmUserActionDialog, {
             member: this.props.member,
             action: _t('Unban'),
             danger: false,
@@ -56,7 +56,7 @@ const BannedUser = React.createClass({
                 ).catch((err) => {
                     const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                     console.error("Failed to unban: " + err);
-                    Modal.createDialog(ErrorDialog, {
+                    Modal.createTrackedDialog('Failed to unban', '', ErrorDialog, {
                         title: _t('Error'),
                         description: _t('Failed to unban'),
                     });
@@ -402,7 +402,7 @@ module.exports = React.createClass({
         ev.preventDefault();
         var value = ev.target.value;
 
-        Modal.createDialog(QuestionDialog, {
+        Modal.createTrackedDialog('Privacy warning', '', QuestionDialog, {
             title: _t('Privacy warning'),
             description:
                 <div>
@@ -506,7 +506,7 @@ module.exports = React.createClass({
         }, function(err) {
             var errCode = err.errcode || _t('unknown error code');
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
                 title: _t('Error'),
                 description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
             });
@@ -517,7 +517,7 @@ module.exports = React.createClass({
         if (!this.refs.encrypt.checked) return;
 
         var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-        Modal.createDialog(QuestionDialog, {
+        Modal.createTrackedDialog('E2E Enable Warning', '', QuestionDialog, {
             title: _t('Warning!'),
             description: (
                 <div>
diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js
index 7bc551477e..16e768a23f 100644
--- a/src/components/views/settings/AddPhoneNumber.js
+++ b/src/components/views/settings/AddPhoneNumber.js
@@ -82,7 +82,7 @@ export default withMatrixClient(React.createClass({
         }).catch((err) => {
             console.error("Unable to add phone number: " + err);
             let msg = err.message;
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Add Phone Number Error', '', ErrorDialog, {
                 title: _t("Error"),
                 description: msg,
             });
@@ -107,7 +107,7 @@ export default withMatrixClient(React.createClass({
             }
             msgElements.push(<div key="_error" className="error">{msg}</div>);
         }
-        Modal.createDialog(TextInputDialog, {
+        Modal.createTrackedDialog('Prompt for MSISDN Verification Code', '', TextInputDialog, {
             title: _t("Enter Code"),
             description: <div>{msgElements}</div>,
             button: _t("Submit"),
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index 14ec9806b4..f3c0d9033c 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -104,7 +104,7 @@ module.exports = React.createClass({
         }
 
         const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-        Modal.createDialog(QuestionDialog, {
+        Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
             title: _t("Warning!"),
             description:
                 <div>
@@ -164,7 +164,7 @@ module.exports = React.createClass({
         const deferred = Promise.defer();
         // Ask for an email otherwise the user has no way to reset their password
         const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
-        Modal.createDialog(SetEmailDialog, {
+        Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
             title: _t('Do you want to set an email address?'),
             onFinished: (confirmed) => {
                 // ignore confirmed, setting an email is optional
@@ -175,15 +175,13 @@ module.exports = React.createClass({
     },
 
     _onExportE2eKeysClicked: function() {
-        Modal.createDialogAsync(
-            (cb) => {
-                require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
-                    cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
-                }, "e2e-export");
-            }, {
-                matrixClient: MatrixClientPeg.get(),
-            }
-        );
+        Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', (cb) => {
+            require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
+                cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
+            }, "e2e-export");
+        }, {
+            matrixClient: MatrixClientPeg.get(),
+        });
     },
 
     onClickChange: function() {
diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js
index f295a7c2d5..69534f09b1 100644
--- a/src/components/views/settings/DevicesPanelEntry.js
+++ b/src/components/views/settings/DevicesPanelEntry.js
@@ -71,8 +71,8 @@ export default class DevicesPanelEntry extends React.Component {
             // pop up an interactive auth dialog
             var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
 
-            Modal.createDialog(InteractiveAuthDialog, {
-            	title: _t("Authentication"),
+            Modal.createTrackedDialog('Delete Device Dialog', InteractiveAuthDialog, {
+                title: _t("Authentication"),
                 matrixClient: MatrixClientPeg.get(),
                 authData: error.data,
                 makeRequest: this._makeDeleteRequest,
diff --git a/src/createRoom.js b/src/createRoom.js
index 74e4b3c2fc..944c6a70a1 100644
--- a/src/createRoom.js
+++ b/src/createRoom.js
@@ -115,7 +115,7 @@ function createRoom(opts) {
             action: 'join_room_error',
         });
         console.error("Failed to create room " + roomId + " " + err);
-        Modal.createDialog(ErrorDialog, {
+        Modal.createTrackedDialog('Failure to create room', '', ErrorDialog, {
             title: _t("Failure to create room"),
             description: _t("Server may be unavailable, overloaded, or you hit a bug."),
         });
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 0d5b7d9d96..83be31de4b 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -346,6 +346,7 @@
     "Hangup": "Hangup",
     "Hide Apps": "Hide Apps",
     "Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)",
+    "Hide avatar and display name changes": "Hide avatar and display name changes",
     "Hide read receipts": "Hide read receipts",
     "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
     "Historical": "Historical",
@@ -575,6 +576,7 @@
     "To configure the room": "To configure the room",
     "to demote": "to demote",
     "to favourite": "to favourite",
+    "To get started, please pick a username!": "To get started, please pick a username!",
     "To invite users into the room": "To invite users into the room",
     "To kick users": "To kick users",
     "To link to a room it must have <a>an address</a>.": "To link to a room it must have <a>an address</a>.",
diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js
index afc8fdc596..1501e28875 100644
--- a/src/shouldHideEvent.js
+++ b/src/shouldHideEvent.js
@@ -14,38 +14,37 @@
  limitations under the License.
  */
 
-function _isLeaveOrJoin(ev) {
-    const isMemberEvent = ev.getType() === 'm.room.member' && ev.getStateKey() !== undefined;
-    if (!isMemberEvent) {
-        return false; // bail early: all the checks below concern member events only
-    }
+function memberEventDiff(ev) {
+    const diff = {
+        isMemberEvent: ev.getType() === 'm.room.member',
+    };
 
-    // TODO: These checks are done to make sure we're dealing with membership transitions not avatar changes / dupe joins
-    //       These checks are also being done in TextForEvent and should really reside in the JS SDK as a helper function
-    const membership = ev.getContent().membership;
-    const prevMembership = ev.getPrevContent().membership;
-    if (membership === prevMembership && membership === 'join') {
-        // join -> join : This happens when display names change / avatars are set / genuine dupe joins with no changes.
-        //                Find out which we're dealing with.
-        if (ev.getPrevContent().displayname !== ev.getContent().displayname) {
-            return false; // display name changed
-        }
-        if (ev.getPrevContent().avatar_url !== ev.getContent().avatar_url) {
-            return false; // avatar url changed
-        }
-        // dupe join event, fall through to hide rules
-    }
+    // If is not a Member Event then the other checks do not apply, so bail early.
+    if (!diff.isMemberEvent) return diff;
 
+    const content = ev.getContent();
+    const prevContent = ev.getPrevContent();
 
-    // this only applies to joins/invited joins/leaves not invites/kicks/bans
-    const isJoin = membership === 'join' && prevMembership !== 'ban';
-    const isLeave = membership === 'leave' && ev.getStateKey() === ev.getSender();
-    return isJoin || isLeave;
+    diff.isJoin = content.membership === 'join' && prevContent.membership !== 'ban';
+    diff.isPart = content.membership === 'leave' && ev.getStateKey() === ev.getSender();
+
+    const isJoinToJoin = content.membership === prevContent.membership && content.membership === 'join';
+    diff.isDisplaynameChange = isJoinToJoin && content.displayname !== prevContent.displayname;
+    diff.isAvatarChange = isJoinToJoin && content.avatar_url !== prevContent.avatar_url;
+    return diff;
 }
 
-export default function(ev, syncedSettings) {
+export default function shouldHideEvent(ev, syncedSettings) {
     // Hide redacted events
     if (syncedSettings['hideRedactions'] && ev.isRedacted()) return true;
-    if (syncedSettings['hideJoinLeaves'] && _isLeaveOrJoin(ev)) return true;
+
+    const eventDiff = memberEventDiff(ev);
+
+    if (eventDiff.isMemberEvent) {
+        if (syncedSettings['hideJoinLeaves'] && (eventDiff.isJoin || eventDiff.isPart)) return true;
+        const isMemberAvatarDisplaynameChange = eventDiff.isAvatarChange || eventDiff.isDisplaynameChange;
+        if (syncedSettings['hideAvatarDisplaynameChanges'] && isMemberAvatarDisplaynameChange) return true;
+    }
+
     return false;
 }
diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js
index 865caa8997..bd9d3ea0fa 100644
--- a/src/stores/RoomViewStore.js
+++ b/src/stores/RoomViewStore.js
@@ -221,7 +221,7 @@ class RoomViewStore extends Store {
             });
             const msg = err.message ? err.message : JSON.stringify(err);
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-            Modal.createDialog(ErrorDialog, {
+            Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
                 title: _t("Failed to join room"),
                 description: msg,
             });