From 665f507537602878bff2373490ede4a7366ae482 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 21 Apr 2017 18:18:35 +0100
Subject: [PATCH 01/73] Update js-sdk dependency

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 0a0a51fc0b..67dfa165af 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "isomorphic-fetch": "^2.2.1",
     "linkifyjs": "^2.1.3",
     "lodash": "^4.13.1",
-    "matrix-js-sdk": "0.7.6",
+    "matrix-js-sdk": "0.7.7-rc.1",
     "optimist": "^0.6.1",
     "q": "^1.4.1",
     "react": "^15.4.0",

From 2f08340ff0ee2bf767998fbe6d8e2bfee7187752 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 21 Apr 2017 18:22:39 +0100
Subject: [PATCH 02/73] Prepare changelog for v0.8.8-rc.1

---
 CHANGELOG.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 292e60607d..7d4a69fb5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+Changes in [0.8.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.1) (2017-04-21)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7...v0.8.8-rc.1)
+
+ * Update js-sdk to fix registration without a captcha (https://github.com/vector-im/riot-web/issues/3621)
+
+
 Changes in [0.8.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7) (2017-04-12)
 ===================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.4...v0.8.7)

From a55eb00dad61e6485ace58737d44a898c08a41f2 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 21 Apr 2017 18:22:39 +0100
Subject: [PATCH 03/73] v0.8.8-rc.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 67dfa165af..38d08344e0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "0.8.7",
+  "version": "0.8.8-rc.1",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {

From 0590ce7faf9680d9d720a43de786545e7da5e7e6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 23 Apr 2017 06:06:23 +0100
Subject: [PATCH 04/73] Conform damn you (mostly)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/Notifier.js | 53 ++++++++++++++++++++++++-------------------------
 1 file changed, 26 insertions(+), 27 deletions(-)

diff --git a/src/Notifier.js b/src/Notifier.js
index 92770877b7..617135a2c8 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-var MatrixClientPeg = require("./MatrixClientPeg");
-var PlatformPeg = require("./PlatformPeg");
-var TextForEvent = require('./TextForEvent');
-var Avatar = require('./Avatar');
-var dis = require("./dispatcher");
+import MatrixClientPeg from './MatrixClientPeg';
+import PlatformPeg from './PlatformPeg';
+import TextForEvent from './TextForEvent';
+import Avatar from './Avatar';
+import dis from './dispatcher';
 
 /*
  * Dispatches:
@@ -29,7 +29,7 @@ var dis = require("./dispatcher");
  * }
  */
 
-var Notifier = {
+const Notifier = {
     notifsByRoom: {},
 
     notificationMessageForEvent: function(ev) {
@@ -48,16 +48,16 @@ var Notifier = {
             return;
         }
 
-        var msg = this.notificationMessageForEvent(ev);
+        let msg = this.notificationMessageForEvent(ev);
         if (!msg) return;
 
-        var title;
-        if (!ev.sender || room.name == ev.sender.name) {
+        let title;
+        if (!ev.sender || room.name === ev.sender.name) {
             title = room.name;
             // notificationMessageForEvent includes sender,
             // but we already have the sender here
             if (ev.getContent().body) msg = ev.getContent().body;
-        } else if (ev.getType() == 'm.room.member') {
+        } else if (ev.getType() === 'm.room.member') {
             // context is all in the message here, we don't need
             // to display sender info
             title = room.name;
@@ -68,7 +68,7 @@ var Notifier = {
             if (ev.getContent().body) msg = ev.getContent().body;
         }
 
-        var avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
+        const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
             ev.sender, 40, 40, 'crop'
         ) : null;
 
@@ -83,7 +83,7 @@ var Notifier = {
     },
 
     _playAudioNotification: function(ev, room) {
-        var e = document.getElementById("messageAudio");
+        const e = document.getElementById("messageAudio");
         if (e) {
             e.load();
             e.play();
@@ -95,7 +95,7 @@ var Notifier = {
         this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
         this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
         MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
-        MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
+        MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
         MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
         this.toolbarHidden = false;
         this.isSyncing = false;
@@ -104,7 +104,7 @@ var Notifier = {
     stop: function() {
         if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
             MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
-            MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
+            MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
             MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
         }
         this.isSyncing = false;
@@ -121,7 +121,7 @@ var Notifier = {
         // make sure that we persist the current setting audio_enabled setting
         // before changing anything
         if (global.localStorage) {
-            if(global.localStorage.getItem('audio_notifications_enabled') == null) {
+            if (global.localStorage.getItem('audio_notifications_enabled') === null) {
                 this.setAudioEnabled(this.isEnabled());
             }
         }
@@ -141,7 +141,7 @@ var Notifier = {
                 if (callback) callback();
                 dis.dispatch({
                     action: "notifier_enabled",
-                    value: true
+                    value: true,
                 });
             });
             // clear the notifications_hidden flag, so that if notifications are
@@ -152,7 +152,7 @@ var Notifier = {
             global.localStorage.setItem('notifications_enabled', 'false');
             dis.dispatch({
                 action: "notifier_enabled",
-                value: false
+                value: false,
             });
         }
     },
@@ -165,7 +165,7 @@ var Notifier = {
 
         if (!global.localStorage) return true;
 
-        var enabled = global.localStorage.getItem('notifications_enabled');
+        const enabled = global.localStorage.getItem('notifications_enabled');
         if (enabled === null) return true;
         return enabled === 'true';
     },
@@ -173,12 +173,12 @@ var Notifier = {
     setAudioEnabled: function(enable) {
         if (!global.localStorage) return;
         global.localStorage.setItem('audio_notifications_enabled',
-                                    enable ? 'true' : 'false');
+            enable ? 'true' : 'false');
     },
 
     isAudioEnabled: function(enable) {
         if (!global.localStorage) return true;
-        var enabled = global.localStorage.getItem(
+        const enabled = global.localStorage.getItem(
             'audio_notifications_enabled');
         // default to true if the popups are enabled
         if (enabled === null) return this.isEnabled();
@@ -192,7 +192,7 @@ var Notifier = {
         // this is nothing to do with notifier_enabled
         dis.dispatch({
             action: "notifier_enabled",
-            value: this.isEnabled()
+            value: this.isEnabled(),
         });
 
         // update the info to localStorage for persistent settings
@@ -215,8 +215,7 @@ var Notifier = {
     onSyncStateChange: function(state) {
         if (state === "SYNCING") {
             this.isSyncing = true;
-        }
-        else if (state === "STOPPED" || state === "ERROR") {
+        } else if (state === "STOPPED" || state === "ERROR") {
             this.isSyncing = false;
         }
     },
@@ -225,10 +224,10 @@ var Notifier = {
         if (toStartOfTimeline) return;
         if (!room) return;
         if (!this.isSyncing) return; // don't alert for any messages initially
-        if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
+        if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
         if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
 
-        var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
+        const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
         if (actions && actions.notify) {
             if (this.isEnabled()) {
                 this._displayPopupNotification(ev, room);
@@ -240,7 +239,7 @@ var Notifier = {
     },
 
     onRoomReceipt: function(ev, room) {
-        if (room.getUnreadNotificationCount() == 0) {
+        if (room.getUnreadNotificationCount() === 0) {
             // ideally we would clear each notification when it was read,
             // but we have no way, given a read receipt, to know whether
             // the receipt comes before or after an event, so we can't
@@ -255,7 +254,7 @@ var Notifier = {
             }
             delete this.notifsByRoom[room.roomId];
         }
-    }
+    },
 };
 
 if (!global.mxNotifier) {

From 5e8b43f3edc70cd8d73a920c9a231c0f681c43fb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 23 Apr 2017 06:16:25 +0100
Subject: [PATCH 05/73] if we're not granted, show an ErrorDialog with some
 text which needs changing

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/Notifier.js | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/Notifier.js b/src/Notifier.js
index 617135a2c8..fed2760732 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -20,6 +20,8 @@ import PlatformPeg from './PlatformPeg';
 import TextForEvent from './TextForEvent';
 import Avatar from './Avatar';
 import dis from './dispatcher';
+import sdk from './index';
+import Modal from './Modal';
 
 /*
  * Dispatches:
@@ -131,6 +133,14 @@ const Notifier = {
             plaf.requestNotificationPermission().done((result) => {
                 if (result !== 'granted') {
                     // The permission request was dismissed or denied
+                    const description = result === 'denied'
+                        ? 'Your browser is not permitting this app to send you notifications.'
+                        : 'It seems you didn\'t accept notifications when your browser asked';
+                    const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
+                    Modal.createDialog(ErrorDialog, {
+                        title: 'Unable to enable Notifications',
+                        description,
+                    });
                     return;
                 }
 

From 74e92d6c235e629802184c86d0323587dec9f82f Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 24 Apr 2017 15:44:45 +0100
Subject: [PATCH 06/73] Remove DM-guessing code

---
 src/components/views/rooms/RoomList.js | 53 +++-----------------------
 1 file changed, 6 insertions(+), 47 deletions(-)

diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 3810f7d4d6..5839b66d1c 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -97,7 +97,7 @@ module.exports = React.createClass({
             if (this.props.selectedRoom) {
                 constantTimeDispatcher.dispatch(
                     "RoomTile.select", this.props.selectedRoom, {}
-                );            
+                );
             }
             constantTimeDispatcher.dispatch(
                 "RoomTile.select", nextProps.selectedRoom, { selected: true }
@@ -265,7 +265,7 @@ module.exports = React.createClass({
     },
 
     onRoomStateMember: function(ev, state, member) {
-        if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && 
+        if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
             ev.getPrevContent() && ev.getPrevContent().membership === "invite")
         {
             this._delayedRefreshRoomList();
@@ -290,7 +290,7 @@ module.exports = React.createClass({
             this._delayedRefreshRoomList();
         }
         else if (ev.getType() == 'm.push_rules') {
-            this._delayedRefreshRoomList();            
+            this._delayedRefreshRoomList();
         }
     },
 
@@ -318,7 +318,7 @@ module.exports = React.createClass({
         // as needed.
         // Alternatively we'd do something magical with Immutable.js or similar.
         this.setState(this.getRoomLists());
-        
+
         // this._lastRefreshRoomListTs = Date.now();
     },
 
@@ -341,7 +341,7 @@ module.exports = React.createClass({
         MatrixClientPeg.get().getRooms().forEach(function(room) {
             const me = room.getMember(MatrixClientPeg.get().credentials.userId);
             if (!me) return;
-            
+
             // console.log("room = " + room.name + ", me.membership = " + me.membership +
             //             ", sender = " + me.events.member.getSender() +
             //             ", target = " + me.events.member.getStateKey() +
@@ -391,51 +391,10 @@ module.exports = React.createClass({
             }
         });
 
-        if (s.lists["im.vector.fake.direct"].length == 0 &&
-            MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
-            !MatrixClientPeg.get().isGuest())
-        {
-            // scan through the 'recents' list for any rooms which look like DM rooms
-            // and make them DM rooms
-            const oldRecents = s.lists["im.vector.fake.recent"];
-            s.lists["im.vector.fake.recent"] = [];
-
-            for (const room of oldRecents) {
-                const me = room.getMember(MatrixClientPeg.get().credentials.userId);
-
-                if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
-                    self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
-                    s.lists["im.vector.fake.direct"].push(room);
-                } else {
-                    self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
-                    s.lists["im.vector.fake.recent"].push(room);
-                }
-            }
-
-            // save these new guessed DM rooms into the account data
-            const newMDirectEvent = {};
-            for (const room of s.lists["im.vector.fake.direct"]) {
-                const me = room.getMember(MatrixClientPeg.get().credentials.userId);
-                const otherPerson = Rooms.getOnlyOtherMember(room, me);
-                if (!otherPerson) continue;
-
-                const roomList = newMDirectEvent[otherPerson.userId] || [];
-                roomList.push(room.roomId);
-                newMDirectEvent[otherPerson.userId] = roomList;
-            }
-
-            console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent));
-
-            // if this fails, fine, we'll just do the same thing next time we get the room lists
-            MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
-        }
-
-        //console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
-
         // we actually apply the sorting to this when receiving the prop in RoomSubLists.
 
         // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
-/*        
+/*
         this.listOrder = [
             "im.vector.fake.invite",
             "m.favourite",

From bb6dd363d70d9ebeaa650a8349c233251c0b00be Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 10 Apr 2017 12:07:39 +0100
Subject: [PATCH 07/73] unbreak in-app permalinks correctly

---
 src/linkify-matrix.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js
index e085b1a27a..c8e20316a9 100644
--- a/src/linkify-matrix.js
+++ b/src/linkify-matrix.js
@@ -122,7 +122,7 @@ var escapeRegExp = function(string) {
 // anyone else really should be using matrix.to.
 matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
     + escapeRegExp(window.location.host + window.location.pathname) + "|"
-    + "(?:www\\.)?(riot|vector)\\.im/(?:beta|staging|develop)/"
+    + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/"
     + ")(#.*)";
 
 matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";

From cf2cf66caebea125912b1ff228935d2fc2497213 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Sat, 22 Apr 2017 21:06:38 +0100
Subject: [PATCH 08/73] fix deep-linking to riot.im/app

---
 src/linkify-matrix.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js
index c8e20316a9..d9b0b78982 100644
--- a/src/linkify-matrix.js
+++ b/src/linkify-matrix.js
@@ -122,7 +122,7 @@ var escapeRegExp = function(string) {
 // anyone else really should be using matrix.to.
 matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
     + escapeRegExp(window.location.host + window.location.pathname) + "|"
-    + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/"
+    + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/"
     + ")(#.*)";
 
 matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";

From 8e6981db44c7a765cc26f21c3fde76d5a723debe Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 24 Apr 2017 18:24:28 +0100
Subject: [PATCH 09/73] Prepare changelog for v0.8.8-rc.2

---
 CHANGELOG.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d4a69fb5b..32f16c46ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+Changes in [0.8.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.2) (2017-04-24)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.1...v0.8.8-rc.2)
+
+ * Fix bug where links to Riot would fail to open.
+
+
 Changes in [0.8.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.1) (2017-04-21)
 =============================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7...v0.8.8-rc.1)

From e569144d6f700cf05e52f67b66aa92237ee7546f Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 24 Apr 2017 18:24:29 +0100
Subject: [PATCH 10/73] v0.8.8-rc.2

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 38d08344e0..5fc8bc2750 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "0.8.8-rc.1",
+  "version": "0.8.8-rc.2",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {

From 74dabb20873cb4650041710085d2ed06b3476218 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 25 Apr 2017 10:52:44 +0100
Subject: [PATCH 11/73] Released js-sdk

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5fc8bc2750..d14bc2b766 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "isomorphic-fetch": "^2.2.1",
     "linkifyjs": "^2.1.3",
     "lodash": "^4.13.1",
-    "matrix-js-sdk": "0.7.7-rc.1",
+    "matrix-js-sdk": "0.7.7",
     "optimist": "^0.6.1",
     "q": "^1.4.1",
     "react": "^15.4.0",

From f6fd7b04ac01d7a7e3ef83ca186d9c462b047cb1 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 25 Apr 2017 10:53:58 +0100
Subject: [PATCH 12/73] Prepare changelog for v0.8.8

---
 CHANGELOG.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32f16c46ae..97dda666de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)
+
+ * No changes
+
+
 Changes in [0.8.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.2) (2017-04-24)
 =============================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.1...v0.8.8-rc.2)

From d81adb234a7581169a2ddbf2f0ee6375e5c340ce Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 25 Apr 2017 10:53:59 +0100
Subject: [PATCH 13/73] v0.8.8

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d14bc2b766..00dc902cc9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "0.8.8-rc.2",
+  "version": "0.8.8",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {

From c9c72036f38e500a88f5b9e0b7b57808a583f0ab Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Fri, 28 Apr 2017 01:00:10 +0100
Subject: [PATCH 14/73] Change wording

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/Notifier.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/Notifier.js b/src/Notifier.js
index fed2760732..f68b7e562c 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -134,8 +134,10 @@ const Notifier = {
                 if (result !== 'granted') {
                     // The permission request was dismissed or denied
                     const description = result === 'denied'
-                        ? 'Your browser is not permitting this app to send you notifications.'
-                        : 'It seems you didn\'t accept notifications when your browser asked';
+                        ? 'Riot does not have permission to send you notifications'
+                        + ' - please check your browser settings'
+                        : 'Riot was not given permission to send notifications'
+                        + '- please try again';
                     const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
                     Modal.createDialog(ErrorDialog, {
                         title: 'Unable to enable Notifications',

From 47827e0b81c8db6977d2a628451daf9db348c5b5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Fri, 28 Apr 2017 01:00:50 +0100
Subject: [PATCH 15/73] un-eat the space

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/Notifier.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Notifier.js b/src/Notifier.js
index f68b7e562c..6473ab4d9c 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -137,7 +137,7 @@ const Notifier = {
                         ? 'Riot does not have permission to send you notifications'
                         + ' - please check your browser settings'
                         : 'Riot was not given permission to send notifications'
-                        + '- please try again';
+                        + ' - please try again';
                     const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
                     Modal.createDialog(ErrorDialog, {
                         title: 'Unable to enable Notifications',

From f5f35e32946a6690615eaecffe77c723f8567950 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Wed, 26 Apr 2017 18:59:16 +0100
Subject: [PATCH 16/73] Make the left panel more friendly to new users

https://github.com/vector-im/riot-web/issues/3609
---
 src/components/views/rooms/RoomList.js | 118 +++++++++++++++++++------
 1 file changed, 89 insertions(+), 29 deletions(-)

diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 96ff65498f..39d3406b73 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -29,7 +29,14 @@ import DMRoomMap from '../../../utils/DMRoomMap';
 var Receipt = require('../../../utils/Receipt');
 var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
 
-var HIDE_CONFERENCE_CHANS = true;
+const HIDE_CONFERENCE_CHANS = true;
+
+const VERBS = {
+    'm.favourite': 'favourite',
+    'im.vector.fake.direct': 'tag direct chat',
+    'im.vector.fake.recent': 'restore',
+    'm.lowpriority': 'demote',
+};
 
 module.exports = React.createClass({
     displayName: 'RoomList',
@@ -53,6 +60,7 @@ module.exports = React.createClass({
     getInitialState: function() {
         return {
             isLoadingLeftRooms: false,
+            totalRoomCount: null,
             lists: {},
             incomingCall: null,
         };
@@ -73,8 +81,7 @@ module.exports = React.createClass({
         // lookup for which lists a given roomId is currently in.
         this.listsForRoomId = {};
 
-        var s = this.getRoomLists();
-        this.setState(s);
+        this.refreshRoomList();
 
         // order of the sublists
         //this.listOrder = [];
@@ -317,21 +324,29 @@ module.exports = React.createClass({
         // any changes to it incrementally, updating the appropriate sublists
         // as needed.
         // Alternatively we'd do something magical with Immutable.js or similar.
-        this.setState(this.getRoomLists());
+        const lists = this.getRoomLists();
+        let totalRooms = 0;
+        for (const l of Object.values(lists)) {
+            totalRooms += l.length;
+        }
+        this.setState({
+            lists: this.getRoomLists(),
+            totalRoomCount: totalRooms,
+        });
         
         // this._lastRefreshRoomListTs = Date.now();
     },
 
     getRoomLists: function() {
         var self = this;
-        var s = { lists: {} };
+        const lists = {};
 
-        s.lists["im.vector.fake.invite"] = [];
-        s.lists["m.favourite"] = [];
-        s.lists["im.vector.fake.recent"] = [];
-        s.lists["im.vector.fake.direct"] = [];
-        s.lists["m.lowpriority"] = [];
-        s.lists["im.vector.fake.archived"] = [];
+        lists["im.vector.fake.invite"] = [];
+        lists["m.favourite"] = [];
+        lists["im.vector.fake.recent"] = [];
+        lists["im.vector.fake.direct"] = [];
+        lists["m.lowpriority"] = [];
+        lists["im.vector.fake.archived"] = [];
 
         this.listsForRoomId = {};
         var otherTagNames = {};
@@ -353,7 +368,7 @@ module.exports = React.createClass({
 
             if (me.membership == "invite") {
                 self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
-                s.lists["im.vector.fake.invite"].push(room);
+                lists["im.vector.fake.invite"].push(room);
             }
             else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
                 // skip past this room & don't put it in any lists
@@ -366,8 +381,8 @@ module.exports = React.createClass({
                 if (tagNames.length) {
                     for (var i = 0; i < tagNames.length; i++) {
                         var tagName = tagNames[i];
-                        s.lists[tagName] = s.lists[tagName] || [];
-                        s.lists[tagName].push(room);
+                        lists[tagName] = lists[tagName] || [];
+                        lists[tagName].push(room);
                         self.listsForRoomId[room.roomId].push(tagName);
                         otherTagNames[tagName] = 1;
                     }
@@ -375,46 +390,46 @@ module.exports = React.createClass({
                 else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
                     // "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
                     self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
-                    s.lists["im.vector.fake.direct"].push(room);
+                    lists["im.vector.fake.direct"].push(room);
                 }
                 else {
                     self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
-                    s.lists["im.vector.fake.recent"].push(room);
+                    lists["im.vector.fake.recent"].push(room);
                 }
             }
             else if (me.membership === "leave") {
                 self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
-                s.lists["im.vector.fake.archived"].push(room);
+                lists["im.vector.fake.archived"].push(room);
             }
             else {
                 console.error("unrecognised membership: " + me.membership + " - this should never happen");
             }
         });
 
-        if (s.lists["im.vector.fake.direct"].length == 0 &&
+        if (lists["im.vector.fake.direct"].length == 0 &&
             MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
             !MatrixClientPeg.get().isGuest())
         {
             // scan through the 'recents' list for any rooms which look like DM rooms
             // and make them DM rooms
-            const oldRecents = s.lists["im.vector.fake.recent"];
-            s.lists["im.vector.fake.recent"] = [];
+            const oldRecents = lists["im.vector.fake.recent"];
+            lists["im.vector.fake.recent"] = [];
 
             for (const room of oldRecents) {
                 const me = room.getMember(MatrixClientPeg.get().credentials.userId);
 
                 if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
                     self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
-                    s.lists["im.vector.fake.direct"].push(room);
+                    lists["im.vector.fake.direct"].push(room);
                 } else {
                     self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
-                    s.lists["im.vector.fake.recent"].push(room);
+                    lists["im.vector.fake.recent"].push(room);
                 }
             }
 
             // save these new guessed DM rooms into the account data
             const newMDirectEvent = {};
-            for (const room of s.lists["im.vector.fake.direct"]) {
+            for (const room of lists["im.vector.fake.direct"]) {
                 const me = room.getMember(MatrixClientPeg.get().credentials.userId);
                 const otherPerson = Rooms.getOnlyOtherMember(room, me);
                 if (!otherPerson) continue;
@@ -449,7 +464,7 @@ module.exports = React.createClass({
         ];
 */
 
-        return s;
+        return lists;
     },
 
     _getScrollNode: function() {
@@ -479,6 +494,7 @@ module.exports = React.createClass({
         var incomingCallBox = document.getElementById("incomingCallBox");
         if (incomingCallBox && incomingCallBox.parentElement) {
             var scrollArea = this._getScrollNode();
+            if (!scrollArea) return;
             // Use the offset of the top of the scroll area from the window
             // as this is used to calculate the CSS fixed top position for the stickies
             var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
@@ -502,6 +518,7 @@ module.exports = React.createClass({
     // properly through React
     _initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
         var scrollArea = this._getScrollNode();
+        if (!scrollArea) return;
         // Use the offset of the top of the scroll area from the window
         // as this is used to calculate the CSS fixed top position for the stickies
         var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
@@ -599,6 +616,49 @@ module.exports = React.createClass({
         this.refs.gemscroll.forceUpdate();
     },
 
+    _getEmptyContent: function(section) {
+        let greyed = false;
+        if (this.state.totalRoomCount === 0) {
+            const TintableSvg = sdk.getComponent('elements.TintableSvg');
+            switch (section) {
+                case 'm.favourite':
+                case 'm.lowpriority':
+                    greyed = true;
+                    break;
+                case 'im.vector.fake.direct':
+                    return <div className="mx_RoomList_emptySubListTip">
+                        <div className="mx_RoomList_butonPreview">
+                            <TintableSvg src="img/icons-people.svg" width="25" height="25" />
+                        </div>
+                        Use the button below to chat with someone!
+                    </div>;
+                case 'im.vector.fake.recent':
+                    return <div className="mx_RoomList_emptySubListTip">
+                        <div className="mx_RoomList_butonPreview">
+                            <TintableSvg src="img/icons-directory.svg" width="25" height="25" />
+                        </div>
+                        Use the button below to browse the room directory
+                        <br /><br />
+                        <div className="mx_RoomList_butonPreview">
+                            <TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
+                        </div>
+                        or this button to start a new one!
+                    </div>;
+            }
+        }
+        const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
+
+        const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
+
+        let label;
+        if (greyed) {
+            label = <span className="mx_RoomList_greyedSubListLabel">{labelText}</span>;
+        } else {
+            label = labelText;
+        }
+        return <RoomDropTarget label={label} />;
+    },
+
     render: function() {
         var RoomSubList = sdk.getComponent('structures.RoomSubList');
         var self = this;
@@ -622,7 +682,7 @@ module.exports = React.createClass({
                 <RoomSubList list={ self.state.lists['m.favourite'] }
                              label="Favourites"
                              tagName="m.favourite"
-                             verb="favourite"
+                             emptyContent={this._getEmptyContent('m.favourite')}
                              editable={ true }
                              order="manual"
                              incomingCall={ self.state.incomingCall }
@@ -635,7 +695,7 @@ module.exports = React.createClass({
                 <RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
                              label="People"
                              tagName="im.vector.fake.direct"
-                             verb="tag direct chat"
+                             emptyContent={this._getEmptyContent('im.vector.fake.direct')}
                              editable={ true }
                              order="recent"
                              incomingCall={ self.state.incomingCall }
@@ -650,7 +710,7 @@ module.exports = React.createClass({
                              label="Rooms"
                              tagName="im.vector.fake.recent"
                              editable={ true }
-                             verb="restore"
+                             emptyContent={this._getEmptyContent('im.vector.fake.recent')}
                              order="recent"
                              incomingCall={ self.state.incomingCall }
                              collapsed={ self.props.collapsed }
@@ -665,7 +725,7 @@ module.exports = React.createClass({
                              key={ tagName }
                              label={ tagName }
                              tagName={ tagName }
-                             verb={ "tag as " + tagName }
+                             emptyContent={this._getEmptyContent(tagName)}
                              editable={ true }
                              order="manual"
                              incomingCall={ self.state.incomingCall }
@@ -681,7 +741,7 @@ module.exports = React.createClass({
                 <RoomSubList list={ self.state.lists['m.lowpriority'] }
                              label="Low priority"
                              tagName="m.lowpriority"
-                             verb="demote"
+                             emptyContent={this._getEmptyContent('m.lowpriority')}
                              editable={ true }
                              order="recent"
                              incomingCall={ self.state.incomingCall }

From 083d5bf4632ca43bc9c255a43946cc6978ab9951 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 28 Apr 2017 11:20:29 +0100
Subject: [PATCH 17/73] Other empty sections no longer need to be greyed

---
 src/components/views/rooms/RoomList.js | 13 +------------
 1 file changed, 1 insertion(+), 12 deletions(-)

diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 39d3406b73..963f5ad425 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -617,14 +617,9 @@ module.exports = React.createClass({
     },
 
     _getEmptyContent: function(section) {
-        let greyed = false;
         if (this.state.totalRoomCount === 0) {
             const TintableSvg = sdk.getComponent('elements.TintableSvg');
             switch (section) {
-                case 'm.favourite':
-                case 'm.lowpriority':
-                    greyed = true;
-                    break;
                 case 'im.vector.fake.direct':
                     return <div className="mx_RoomList_emptySubListTip">
                         <div className="mx_RoomList_butonPreview">
@@ -650,13 +645,7 @@ module.exports = React.createClass({
 
         const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
 
-        let label;
-        if (greyed) {
-            label = <span className="mx_RoomList_greyedSubListLabel">{labelText}</span>;
-        } else {
-            label = labelText;
-        }
-        return <RoomDropTarget label={label} />;
+        return <RoomDropTarget label={labelText} />;
     },
 
     render: function() {

From 6685cbcb25e76abd9304319e6f2f16dfc53e58f8 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sat, 29 Apr 2017 06:13:03 +0100
Subject: [PATCH 18/73] make MessageComposerInput (new and old) warn on unload
 new needs binding due to class this ref being softer couldn't do this nicely
 in MessageComposer/Input as isTyping wasn't propagated.

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/rooms/MessageComposerInput.js    | 10 ++++++++++
 src/components/views/rooms/MessageComposerInputOld.js |  9 +++++++++
 2 files changed, 19 insertions(+)

diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 8efd2fa579..672279ef54 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -93,6 +93,7 @@ export default class MessageComposerInput extends React.Component {
         this.onEscape = this.onEscape.bind(this);
         this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
         this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
+        this.onPageUnload = this.onPageUnload.bind(this);
 
         const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
 
@@ -233,11 +234,13 @@ export default class MessageComposerInput extends React.Component {
             this.refs.editor,
             this.props.room.roomId
         );
+        window.addEventListener('beforeunload', this.onPageUnload);
     }
 
     componentWillUnmount() {
         dis.unregister(this.dispatcherRef);
         this.sentHistory.saveLastTextEntry();
+        window.removeEventListener('beforeunload', this.onPageUnload);
     }
 
     componentWillUpdate(nextProps, nextState) {
@@ -249,6 +252,13 @@ export default class MessageComposerInput extends React.Component {
         }
     }
 
+    onPageUnload(event) {
+        if (this.isTyping) {
+            return event.returnValue =
+                'You seem to be typing a message, are you sure you want to quit?';
+        }
+    }
+
     onAction(payload) {
         let editor = this.refs.editor;
         let contentState = this.state.editorState.getCurrentContent();
diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js
index 378644478c..2a992cdf5f 100644
--- a/src/components/views/rooms/MessageComposerInputOld.js
+++ b/src/components/views/rooms/MessageComposerInputOld.js
@@ -177,11 +177,20 @@ export default React.createClass({
         if (this.props.tabComplete) {
             this.props.tabComplete.setTextArea(this.refs.textarea);
         }
+        window.addEventListener('beforeunload', this.onPageUnload);
     },
 
     componentWillUnmount: function() {
         dis.unregister(this.dispatcherRef);
         this.sentHistory.saveLastTextEntry();
+        window.removeEventListener('beforeunload', this.onPageUnload);
+    },
+
+    onPageUnload(event) {
+        if (this.isTyping) {
+            return event.returnValue =
+                'You seem to be typing a message, are you sure you want to quit?';
+        }
     },
 
     onAction: function(payload) {

From daae3bd1ecfae565ff1ed7958c564ff73fb0eae0 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sat, 29 Apr 2017 06:25:30 +0100
Subject: [PATCH 19/73] warn on unload when uploading file(s)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/RoomView.js | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index a0c36374b6..9414a59e01 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -271,6 +271,7 @@ module.exports = React.createClass({
 
         this._updateConfCallNotification();
 
+        window.addEventListener('beforeunload', this.onPageUnload);
         window.addEventListener('resize', this.onResize);
         this.onResize();
 
@@ -353,6 +354,7 @@ module.exports = React.createClass({
             MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
         }
 
+        window.removeEventListener('beforeunload', this.onPageUnload);
         window.removeEventListener('resize', this.onResize);
 
         document.removeEventListener("keydown", this.onKeyDown);
@@ -365,6 +367,14 @@ module.exports = React.createClass({
         // Tinter.tint(); // reset colourscheme
     },
 
+    onPageUnload(event) {
+        if (ContentMessages.getCurrentUploads().length > 0) {
+            return event.returnValue =
+                'You seem to be uploading files, are you sure you want to quit?';
+        }
+    },
+
+
     onKeyDown: function(ev) {
         let handled = false;
         const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;

From 86a5ff42e952c52218f68937b83ac179fe290b83 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sat, 29 Apr 2017 14:22:06 +0100
Subject: [PATCH 20/73] Change max-len 90->120

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .eslintrc.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.eslintrc.js b/.eslintrc.js
index 6cd0e1015e..74790a2964 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -64,7 +64,7 @@ module.exports = {
             // to JSX.
             ignorePattern: '^\\s*<',
             ignoreComments: true,
-            code: 90,
+            code: 120,
         }],
         "valid-jsdoc": ["warn"],
         "new-cap": ["warn"],

From c8fb18dc932bdfe6e64bce328b515a52d6c32607 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Sun, 30 Apr 2017 13:00:47 +0100
Subject: [PATCH 21/73] Pin filesize ver to fix break upstream

https://travis-ci.org/vector-im/riot-web/builds/227340622
https://github.com/avoidwork/filesize.js/issues/87
3.5.7 and 3.5.8 ver released <24h ago and broke stuff for us
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5c96a74f5b..95a82bbb73 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "draft-js-export-markdown": "^0.2.0",
     "emojione": "2.2.3",
     "file-saver": "^1.3.3",
-    "filesize": "^3.1.2",
+    "filesize": "3.5.6",
     "flux": "^2.0.3",
     "fuse.js": "^2.2.0",
     "glob": "^5.0.14",

From 3f25928380b36834e0cbe249938581ede9dc3c6e Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Tue, 2 May 2017 16:34:39 +0100
Subject: [PATCH 22/73] Fix jumping to an unread event when in MELS

This adds the `data-contained-scroll-tokens` API to elements in `ScrollPanel` which allows arbitrary containers of elements with scroll tokens to declare their contained scroll tokens. When jumping to a scroll token inside a container, the `ScrollPanel` will act as if it is scrolling to the container itself, not the child.

MELS has been modified such that it exposes the scroll tokens of all events that exist within it.This means "Jump to unread message" will work if the unread event is within a MELS (which is any member event, because even individual member events surrounded by other events are put inside a MELS).
---
 src/components/structures/MessagePanel.js               | 1 -
 src/components/structures/ScrollPanel.js                | 5 +++++
 src/components/views/elements/MemberEventListSummary.js | 5 +++--
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 246f351841..74aec61511 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -354,7 +354,6 @@ module.exports = React.createClass({
                     <MemberEventListSummary
                         key={key}
                         events={summarisedEvents}
-                        data-scroll-token={eventId}
                         onToggle={this._onWidgetLoad} // Update scroll state
                     >
                             {eventTiles}
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index d43e22e2f1..da3b303d89 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -551,6 +551,11 @@ module.exports = React.createClass({
         var messages = this.refs.itemlist.children;
         for (var i = messages.length-1; i >= 0; --i) {
             var m = messages[i];
+            if (m.dataset.containedScrollTokens &&
+                m.dataset.containedScrollTokens.indexOf(scrollToken) !== -1) {
+                node = m;
+                break;
+            }
             if (!m.dataset.scrollToken) continue;
             if (m.dataset.scrollToken == scrollToken) {
                 node = m;
diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js
index 63bd2a7c39..a24e19577d 100644
--- a/src/components/views/elements/MemberEventListSummary.js
+++ b/src/components/views/elements/MemberEventListSummary.js
@@ -369,6 +369,7 @@ module.exports = React.createClass({
 
     render: function() {
         const eventsToRender = this.props.events;
+        const eventIds = eventsToRender.map(e => e.getId());
         const fewEvents = eventsToRender.length < this.props.threshold;
         const expanded = this.state.expanded || fewEvents;
 
@@ -379,7 +380,7 @@ module.exports = React.createClass({
 
         if (fewEvents) {
             return (
-                <div className="mx_MemberEventListSummary">
+                <div className="mx_MemberEventListSummary" data-contained-scroll-tokens={eventIds}>
                     {expandedEvents}
                 </div>
             );
@@ -437,7 +438,7 @@ module.exports = React.createClass({
         );
 
         return (
-            <div className="mx_MemberEventListSummary">
+            <div className="mx_MemberEventListSummary" data-contained-scroll-tokens={eventIds}>
                 {toggleButton}
                 {summaryContainer}
                 {expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null}

From fe83a99ab709121d2259b25d1a735babb2a022ee Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Tue, 2 May 2017 17:36:59 +0100
Subject: [PATCH 23/73] Update ScrollPanel docs

---
 src/components/structures/ScrollPanel.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index da3b303d89..7fa320d784 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -50,6 +50,10 @@ if (DEBUG_SCROLL) {
  * serialise the scroll state, and returned as the 'trackedScrollToken'
  * attribute by getScrollState().
  *
+ * Child elements that contain elements that have scroll tokens must declare the
+ * contained scroll tokens using 'data-contained-scroll-tokens`. When scrolling
+ * to a contained scroll token, the ScrollPanel will scroll to the container.
+ *
  * Some notes about the implementation:
  *
  * The saved 'scrollState' can exist in one of two states:

From 4febc63aeecee7613681749b3aa435cbd25ea17f Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Tue, 2 May 2017 17:41:09 +0100
Subject: [PATCH 24/73] Add comment to _scrollToToken

---
 src/components/structures/ScrollPanel.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 7fa320d784..3f36fac89b 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -555,6 +555,9 @@ module.exports = React.createClass({
         var messages = this.refs.itemlist.children;
         for (var i = messages.length-1; i >= 0; --i) {
             var m = messages[i];
+            // 'data-contained-scroll-tokens' has been set, indicating that a child
+            // element contains elements that each have a token. Check this array of
+            // tokens for `scrollToken`.
             if (m.dataset.containedScrollTokens &&
                 m.dataset.containedScrollTokens.indexOf(scrollToken) !== -1) {
                 node = m;

From af137f8867dce643232c8edfa92294b8f87b3b89 Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Tue, 2 May 2017 18:30:46 +0100
Subject: [PATCH 25/73] Validate phone number on login

To prevent confusion when accidently inputting mxid or email. Fixes https://github.com/vector-im/riot-web/issues/3637
---
 src/components/structures/login/Login.js | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index 315a0ea242..a3635177e2 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -23,6 +23,9 @@ import url from 'url';
 import sdk from '../../../index';
 import Login from '../../../Login';
 
+// For validating phone numbers without country codes
+const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
+
 /**
  * A wire component which glues together login UI components and Login logic
  */
@@ -125,7 +128,16 @@ module.exports = React.createClass({
     },
 
     onPhoneNumberChanged: function(phoneNumber) {
-        this.setState({ phoneNumber: phoneNumber });
+        // Validate the phone number entered
+        if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
+            this.setState({ errorText: 'The phone number entered looks invalid' });
+            return;
+        }
+
+        this.setState({
+            phoneNumber: phoneNumber,
+            errorText: null,
+        });
     },
 
     onServerConfigChange: function(config) {

From 76e98d42679e5a1167ebad419ec66abfd70f7a3c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 May 2017 21:12:58 +0100
Subject: [PATCH 26/73] improve version hyperlinking removed redundant v prefix
 (key already says version) links to most applicable version/tag tag-commit ->
 commit commit1-commit2-commit3 -> commit1 (v)x.y.z -> tag<x.y.z> commit ->
 commit

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/UserSettings.js | 25 +++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index ba5d5780b4..88e6829514 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -32,13 +32,22 @@ import AccessibleButton from '../views/elements/AccessibleButton';
 
 // if this looks like a release, use the 'version' from package.json; else use
 // the git sha. Prepend version with v, to look like riot-web version
-const REACT_SDK_VERSION = 'dist' in package_json ? `v${package_json.version}` : package_json.gitHead || '<local>';
+const REACT_SDK_VERSION = 'dist' in package_json ? package_json.version : package_json.gitHead || '<local>';
 
 // Simple method to help prettify GH Release Tags and Commit Hashes.
-const GHVersionUrl = function(repo, token) {
-    const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`;
-    return `https://github.com/${repo}/${uriTail}`;
-}
+const semVerRegex = /^v?(\d+\.\d+\.\d+)(?:-(?:\d+-g)?(.+))?|$/i;
+const gHVersionLabel = function(repo, token) {
+    const match = token.match(semVerRegex);
+    let url; // assume commit hash
+    if (match && match[1]) { // basic semVer string possibly with commit hash
+        url = (match.length > 1 && match[2])
+            ? `https://github.com/${repo}/commit/${match[2]}`
+            : `https://github.com/${repo}/releases/tag/v${match[1]}`;
+    } else {
+        url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
+    }
+    return <a href={url}>{token}</a>;
+};
 
 // Enumerate some simple 'flip a bit' UI settings (if any).
 // 'id' gives the key name in the im.vector.web.settings account data event
@@ -911,7 +920,7 @@ module.exports = React.createClass({
         // we are using a version old version of olm. We assume the former.
         let olmVersionString = "<not-enabled>";
         if (olmVersion !== undefined) {
-            olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
+            olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
         }
 
         return (
@@ -995,11 +1004,11 @@ module.exports = React.createClass({
                     </div>
                     <div className="mx_UserSettings_advanced">
                         matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>')
-                            ? <a href={ GHVersionUrl('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) }>{REACT_SDK_VERSION}</a>
+                            ? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
                             : REACT_SDK_VERSION
                         }<br/>
                         riot-web version: {(this.state.vectorVersion !== null)
-                            ? <a href={ GHVersionUrl('vector-im/riot-web', this.state.vectorVersion.split('-')[0]) }>{this.state.vectorVersion}</a>
+                            ? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
                             : 'unknown'
                         }<br/>
                         olm version: {olmVersionString}<br/>

From 74b2c86f93db0692311750c2a1d38339a44eb72a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 May 2017 21:17:12 +0100
Subject: [PATCH 27/73] tidy up UserSettings

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/UserSettings.js | 177 +++++++++++-----------
 1 file changed, 88 insertions(+), 89 deletions(-)

diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 88e6829514..4b63c8d888 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -14,25 +14,25 @@ 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 ReactDOM = require('react-dom');
-var sdk = require('../../index');
-var MatrixClientPeg = require("../../MatrixClientPeg");
-var PlatformPeg = require("../../PlatformPeg");
-var Modal = require('../../Modal');
-var dis = require("../../dispatcher");
-var q = require('q');
-var package_json = require('../../../package.json');
-var UserSettingsStore = require('../../UserSettingsStore');
-var GeminiScrollbar = require('react-gemini-scrollbar');
-var Email = require('../../email');
-var AddThreepid = require('../../AddThreepid');
-var SdkConfig = require('../../SdkConfig');
+const React = require('react');
+const ReactDOM = require('react-dom');
+const sdk = require('../../index');
+const MatrixClientPeg = require("../../MatrixClientPeg");
+const PlatformPeg = require("../../PlatformPeg");
+const Modal = require('../../Modal');
+const dis = require("../../dispatcher");
+const q = require('q');
+const packageJson = require('../../../package.json');
+const UserSettingsStore = require('../../UserSettingsStore');
+const GeminiScrollbar = require('react-gemini-scrollbar');
+const Email = require('../../email');
+const AddThreepid = require('../../AddThreepid');
+const SdkConfig = require('../../SdkConfig');
 import AccessibleButton from '../views/elements/AccessibleButton';
 
 // if this looks like a release, use the 'version' from package.json; else use
 // the git sha. Prepend version with v, to look like riot-web version
-const REACT_SDK_VERSION = 'dist' in package_json ? package_json.version : package_json.gitHead || '<local>';
+const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
 
 // Simple method to help prettify GH Release Tags and Commit Hashes.
 const semVerRegex = /^v?(\d+\.\d+\.\d+)(?:-(?:\d+-g)?(.+))?|$/i;
@@ -59,7 +59,7 @@ const SETTINGS_LABELS = [
     },
     {
         id: 'hideReadReceipts',
-        label: 'Hide read receipts'
+        label: 'Hide read receipts',
     },
     {
         id: 'dontSendTypingNotifications',
@@ -115,7 +115,7 @@ const THEMES = [
         id: 'theme',
         label: 'Dark theme',
         value: 'dark',
-    }
+    },
 ];
 
 
@@ -189,7 +189,7 @@ module.exports = React.createClass({
         });
         this._refreshFromServer();
 
-        var syncedSettings = UserSettingsStore.getSyncedSettings();
+        const syncedSettings = UserSettingsStore.getSyncedSettings();
         if (!syncedSettings.theme) {
             syncedSettings.theme = 'light';
         }
@@ -211,16 +211,16 @@ module.exports = React.createClass({
             middleOpacity: 1.0,
         });
         dis.unregister(this.dispatcherRef);
-        let cli = MatrixClientPeg.get();
+        const cli = MatrixClientPeg.get();
         if (cli) {
             cli.removeListener("RoomMember.membership", this._onInviteStateChange);
         }
     },
 
     _refreshFromServer: function() {
-        var self = this;
+        const self = this;
         q.all([
-            UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
+            UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
         ]).done(function(resps) {
             self.setState({
                 avatarUrl: resps[0].avatar_url,
@@ -228,7 +228,7 @@ module.exports = React.createClass({
                 phase: "UserSettings.DISPLAY",
             });
         }, function(error) {
-            var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+            const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Failed to load user settings: " + error);
             Modal.createDialog(ErrorDialog, {
                 title: "Can't load user settings",
@@ -245,7 +245,7 @@ module.exports = React.createClass({
 
     onAvatarPickerClick: function(ev) {
         if (MatrixClientPeg.get().isGuest()) {
-            var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
+            const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
             Modal.createDialog(NeedToRegisterDialog, {
                 title: "Please Register",
                 description: "Guests can't set avatars. Please register.",
@@ -259,8 +259,8 @@ module.exports = React.createClass({
     },
 
     onAvatarSelected: function(ev) {
-        var self = this;
-        var changeAvatar = this.refs.changeAvatar;
+        const self = this;
+        const changeAvatar = this.refs.changeAvatar;
         if (!changeAvatar) {
             console.error("No ChangeAvatar found to upload image to!");
             return;
@@ -269,9 +269,9 @@ module.exports = React.createClass({
             // dunno if the avatar changed, re-check it.
             self._refreshFromServer();
         }, function(err) {
-            var errMsg = (typeof err === "string") ? err : (err.error || "");
+            // const errMsg = (typeof err === "string") ? err : (err.error || "");
             console.error("Failed to set avatar: " + err);
-            var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+            const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             Modal.createDialog(ErrorDialog, {
                 title: "Failed to set avatar",
                 description: ((err && err.message) ? err.message : "Operation failed"),
@@ -280,7 +280,7 @@ module.exports = React.createClass({
     },
 
     onLogoutClicked: function(ev) {
-        var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+        const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
         Modal.createDialog(QuestionDialog, {
             title: "Sign out?",
             description:
@@ -295,7 +295,7 @@ module.exports = React.createClass({
                 <button key="export" className="mx_Dialog_primary"
                         onClick={this._onExportE2eKeysClicked}>
                     Export E2E room keys
-                </button>
+                </button>,
             ],
             onFinished: (confirmed) => {
                 if (confirmed) {
@@ -309,34 +309,33 @@ module.exports = React.createClass({
     },
 
     onPasswordChangeError: function(err) {
-        var errMsg = err.error || "";
+        let errMsg = err.error || "";
         if (err.httpStatus === 403) {
             errMsg = "Failed to change password. Is your password correct?";
-        }
-        else if (err.httpStatus) {
+        } else if (err.httpStatus) {
             errMsg += ` (HTTP status ${err.httpStatus})`;
         }
-        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+        const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
         console.error("Failed to change password: " + errMsg);
         Modal.createDialog(ErrorDialog, {
             title: "Error",
-            description: errMsg
+            description: errMsg,
         });
     },
 
     onPasswordChanged: function() {
-        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+        const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
         Modal.createDialog(ErrorDialog, {
             title: "Success",
             description: `Your password was successfully changed. You will not
                           receive push notifications on other devices until you
-                          log back in to them.`
+                          log back in to them.`,
         });
     },
 
     onUpgradeClicked: function() {
         dis.dispatch({
-            action: "start_upgrade_registration"
+            action: "start_upgrade_registration",
         });
     },
 
@@ -350,11 +349,11 @@ module.exports = React.createClass({
     },
 
     _addEmail: function() {
-        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-        var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+        const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+        const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
 
-        var email_address = this.refs.add_email_input.value;
-        if (!Email.looksValid(email_address)) {
+        const emailAddress = this.refs.add_email_input.value;
+        if (!Email.looksValid(emailAddress)) {
             Modal.createDialog(ErrorDialog, {
                 title: "Invalid Email Address",
                 description: "This doesn't appear to be a valid email address",
@@ -364,7 +363,7 @@ module.exports = React.createClass({
         this._addThreepid = new AddThreepid();
         // we always bind emails when registering, so let's do the
         // same here.
-        this._addThreepid.addEmailAddress(email_address, true).done(() => {
+        this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
             Modal.createDialog(QuestionDialog, {
                 title: "Verification Pending",
                 description: "Please check your email and click on the link it contains. Once this is done, click continue.",
@@ -373,7 +372,7 @@ module.exports = React.createClass({
             });
         }, (err) => {
             this.setState({email_add_pending: false});
-            console.error("Unable to add email address " + email_address + " " + err);
+            console.error("Unable to add email address " + emailAddress + " " + err);
             Modal.createDialog(ErrorDialog, {
                 title: "Unable to add email address",
                 description: ((err && err.message) ? err.message : "Operation failed"),
@@ -427,9 +426,9 @@ module.exports = React.createClass({
             this.setState({email_add_pending: false});
         }, (err) => {
             this.setState({email_add_pending: false});
-            if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
-                var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-                var message = "Unable to verify email address. ";
+            if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
+                const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+                let message = "Unable to verify email address. ";
                 message += "Please check your email and click on the link it contains. Once this is done, click continue.";
                 Modal.createDialog(QuestionDialog, {
                     title: "Verification Pending",
@@ -438,7 +437,7 @@ module.exports = React.createClass({
                     onFinished: this.onEmailDialogFinished,
                 });
             } else {
-                var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+                const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                 console.error("Unable to verify email address: " + err);
                 Modal.createDialog(ErrorDialog, {
                     title: "Unable to verify email address",
@@ -478,17 +477,17 @@ module.exports = React.createClass({
 
     _onRejectAllInvitesClicked: function(rooms, ev) {
         this.setState({
-            rejectingInvites: true
+            rejectingInvites: true,
         });
         // reject the invites
-        let promises = rooms.map((room) => {
+        const promises = rooms.map((room) => {
             return MatrixClientPeg.get().leave(room.roomId);
         });
         // purposefully drop errors to the floor: we'll just have a non-zero number on the UI
         // after trying to reject all the invites.
         q.allSettled(promises).then(() => {
             this.setState({
-                rejectingInvites: false
+                rejectingInvites: false,
             });
         }).done();
     },
@@ -501,7 +500,7 @@ module.exports = React.createClass({
                 }, "e2e-export");
             }, {
                 matrixClient: MatrixClientPeg.get(),
-            }
+            },
         );
     },
 
@@ -513,7 +512,7 @@ module.exports = React.createClass({
                 }, "e2e-export");
             }, {
                 matrixClient: MatrixClientPeg.get(),
-            }
+            },
         );
     },
 
@@ -539,7 +538,7 @@ module.exports = React.createClass({
     },
 
     _renderUserInterfaceSettings: function() {
-        var client = MatrixClientPeg.get();
+        // const client = MatrixClientPeg.get();
 
         return (
             <div>
@@ -558,7 +557,7 @@ module.exports = React.createClass({
             <input id="urlPreviewsDisabled"
                    type="checkbox"
                    defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
-                   onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
+                   onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
             />
             <label htmlFor="urlPreviewsDisabled">
                 Disable inline URL previews by default
@@ -571,7 +570,7 @@ module.exports = React.createClass({
             <input id={ setting.id }
                    type="checkbox"
                    defaultChecked={ this._syncedSettings[setting.id] }
-                   onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
+                   onChange={ (e) => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
             />
             <label htmlFor={ setting.id }>
                 { setting.label }
@@ -586,7 +585,7 @@ module.exports = React.createClass({
                    name={ setting.id }
                    value={ setting.value }
                    defaultChecked={ this._syncedSettings[setting.id] === setting.value }
-                   onChange={ e => {
+                   onChange={ (e) => {
                             if (e.target.checked) {
                                 UserSettingsStore.setSyncedSetting(setting.id, setting.value);
                             }
@@ -629,8 +628,8 @@ module.exports = React.createClass({
                 <h3>Cryptography</h3>
                 <div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
                     <ul>
-                        <li><label>Device ID:</label>             <span><code>{deviceId}</code></span></li>
-                        <li><label>Device key:</label>            <span><code><b>{identityKey}</b></code></span></li>
+                        <li><label>Device ID:</label>                       <span><code>{deviceId}</code></span></li>
+                        <li><label>Device key:</label>                      <span><code><b>{identityKey}</b></code></span></li>
                     </ul>
                     { importExportButtons }
                 </div>
@@ -648,8 +647,8 @@ module.exports = React.createClass({
                    type="checkbox"
                    defaultChecked={ this._localSettings[setting.id] }
                    onChange={
-                        e => {
-                            UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
+                        (e) => {
+                            UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
                             if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
                                 client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
                             }
@@ -663,7 +662,7 @@ module.exports = React.createClass({
     },
 
     _renderDevicesPanel: function() {
-        var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
+        const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
         return (
             <div>
                 <h3>Devices</h3>
@@ -674,7 +673,7 @@ module.exports = React.createClass({
 
     _renderBugReport: function() {
         if (!SdkConfig.get().bug_report_endpoint_url) {
-            return <div />
+            return <div />;
         }
         return (
             <div>
@@ -693,17 +692,17 @@ module.exports = React.createClass({
         // default to enabled if undefined
         if (this.props.enableLabs === false) return null;
 
-        let features = UserSettingsStore.LABS_FEATURES.map(feature => (
+        const features = UserSettingsStore.LABS_FEATURES.map((feature) => (
             <div key={feature.id} className="mx_UserSettings_toggle">
                 <input
                     type="checkbox"
                     id={feature.id}
                     name={feature.id}
                     defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
-                    onChange={e => {
+                    onChange={(e) => {
                         if (MatrixClientPeg.get().isGuest()) {
                             e.target.checked = false;
-                            var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
+                            const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
                             Modal.createDialog(NeedToRegisterDialog, {
                                 title: "Please Register",
                                 description: "Guests can't use labs features. Please register.",
@@ -755,14 +754,14 @@ module.exports = React.createClass({
     },
 
     _renderBulkOptions: function() {
-        let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
+        const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
             return r.hasMembershipState(this._me, "invite");
         });
         if (invitedRooms.length === 0) {
             return null;
         }
 
-        let Spinner = sdk.getComponent("elements.Spinner");
+        const Spinner = sdk.getComponent("elements.Spinner");
 
         let reject = <Spinner />;
         if (!this.state.rejectingInvites) {
@@ -786,9 +785,7 @@ module.exports = React.createClass({
 
     _showSpoiler: function(event) {
         const target = event.target;
-        const hidden = target.getAttribute('data-spoiler');
-
-        target.innerHTML = hidden;
+        target.innerHTML = target.getAttribute('data-spoiler');
 
         const range = document.createRange();
         range.selectNodeContents(target);
@@ -799,12 +796,12 @@ module.exports = React.createClass({
     },
 
     nameForMedium: function(medium) {
-        if (medium == 'msisdn') return 'Phone';
+        if (medium === 'msisdn') return 'Phone';
         return medium[0].toUpperCase() + medium.slice(1);
     },
 
     presentableTextForThreepid: function(threepid) {
-        if (threepid.medium == 'msisdn') {
+        if (threepid.medium === 'msisdn') {
             return '+' + threepid.address;
         } else {
             return threepid.address;
@@ -812,7 +809,7 @@ module.exports = React.createClass({
     },
 
     render: function() {
-        var Loader = sdk.getComponent("elements.Spinner");
+        const Loader = sdk.getComponent("elements.Spinner");
         switch (this.state.phase) {
             case "UserSettings.LOADING":
                 return (
@@ -824,18 +821,18 @@ module.exports = React.createClass({
                 throw new Error("Unknown state.phase => " + this.state.phase);
         }
         // can only get here if phase is UserSettings.DISPLAY
-        var SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
-        var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
-        var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
-        var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
-        var Notifications = sdk.getComponent("settings.Notifications");
-        var EditableText = sdk.getComponent('elements.EditableText');
+        const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
+        const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
+        const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
+        const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
+        const Notifications = sdk.getComponent("settings.Notifications");
+        const EditableText = sdk.getComponent('elements.EditableText');
 
-        var avatarUrl = (
+        const avatarUrl = (
             this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
         );
 
-        var threepidsSection = this.state.threepids.map((val, pidIndex) => {
+        const threepidsSection = this.state.threepids.map((val, pidIndex) => {
             const id = "3pid-" + val.address;
             return (
                 <div className="mx_UserSettings_profileTableRow" key={pidIndex}>
@@ -883,7 +880,7 @@ module.exports = React.createClass({
         threepidsSection.push(addEmailSection);
         threepidsSection.push(addMsisdnSection);
 
-        var accountJsx;
+        let accountJsx;
 
         if (MatrixClientPeg.get().isGuest()) {
             accountJsx = (
@@ -891,8 +888,7 @@ module.exports = React.createClass({
                     Create an account
                 </div>
             );
-        }
-        else {
+        } else {
             accountJsx = (
                 <ChangePassword
                         className="mx_UserSettings_accountTable"
@@ -904,9 +900,9 @@ module.exports = React.createClass({
                         onFinished={this.onPasswordChanged} />
             );
         }
-        var notification_area;
+        let notificationArea;
         if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
-            notification_area = (<div>
+            notificationArea = (<div>
                 <h3>Notifications</h3>
 
                 <div className="mx_UserSettings_section">
@@ -978,7 +974,7 @@ module.exports = React.createClass({
 
                 {this._renderReferral()}
 
-                {notification_area}
+                {notificationArea}
 
                 {this._renderUserInterfaceSettings()}
                 {this._renderLabs()}
@@ -994,7 +990,10 @@ module.exports = React.createClass({
                         Logged in as {this._me}
                     </div>
                     <div className="mx_UserSettings_advanced">
-                        Access Token: <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;click to reveal&gt;</span>
+                        Access Token: <span className="mx_UserSettings_advanced_spoiler"
+                                            onClick={this._showSpoiler}
+                                            data-spoiler={ MatrixClientPeg.get().getAccessToken() }
+                        >&lt;click to reveal&gt;</span>
                     </div>
                     <div className="mx_UserSettings_advanced">
                         Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
@@ -1022,5 +1021,5 @@ module.exports = React.createClass({
                 </GeminiScrollbar>
             </div>
         );
-    }
+    },
 });

From b6fd771b9aaba669ef1b567c6d937f026533c2b4 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 3 May 2017 16:21:35 +0100
Subject: [PATCH 28/73] move implementation to MessageComposer to it applies to
 any future composers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/rooms/MessageComposer.js         | 11 +++++++++++
 src/components/views/rooms/MessageComposerInput.js    | 10 ----------
 src/components/views/rooms/MessageComposerInputOld.js |  9 ---------
 3 files changed, 11 insertions(+), 19 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 88230062fe..2161198142 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -43,6 +43,7 @@ export default class MessageComposer extends React.Component {
         this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
         this.onInputStateChanged = this.onInputStateChanged.bind(this);
         this.onEvent = this.onEvent.bind(this);
+        this.onPageUnload = this.onPageUnload.bind(this);
 
         this.state = {
             autocompleteQuery: '',
@@ -64,12 +65,22 @@ export default class MessageComposer extends React.Component {
         // marked as encrypted.
         // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
         MatrixClientPeg.get().on("event", this.onEvent);
+
+        window.addEventListener('beforeunload', this.onPageUnload);
     }
 
     componentWillUnmount() {
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener("event", this.onEvent);
         }
+        window.removeEventListener('beforeunload', this.onPageUnload);
+    }
+
+    onPageUnload(event) {
+        if (this.messageComposerInput && this.messageComposerInput.isTyping) {
+            return event.returnValue =
+                'You seem to be typing a message, are you sure you want to quit?';
+        }
     }
 
     onEvent(event) {
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 672279ef54..8efd2fa579 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -93,7 +93,6 @@ export default class MessageComposerInput extends React.Component {
         this.onEscape = this.onEscape.bind(this);
         this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
         this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
-        this.onPageUnload = this.onPageUnload.bind(this);
 
         const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
 
@@ -234,13 +233,11 @@ export default class MessageComposerInput extends React.Component {
             this.refs.editor,
             this.props.room.roomId
         );
-        window.addEventListener('beforeunload', this.onPageUnload);
     }
 
     componentWillUnmount() {
         dis.unregister(this.dispatcherRef);
         this.sentHistory.saveLastTextEntry();
-        window.removeEventListener('beforeunload', this.onPageUnload);
     }
 
     componentWillUpdate(nextProps, nextState) {
@@ -252,13 +249,6 @@ export default class MessageComposerInput extends React.Component {
         }
     }
 
-    onPageUnload(event) {
-        if (this.isTyping) {
-            return event.returnValue =
-                'You seem to be typing a message, are you sure you want to quit?';
-        }
-    }
-
     onAction(payload) {
         let editor = this.refs.editor;
         let contentState = this.state.editorState.getCurrentContent();
diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js
index 2a992cdf5f..378644478c 100644
--- a/src/components/views/rooms/MessageComposerInputOld.js
+++ b/src/components/views/rooms/MessageComposerInputOld.js
@@ -177,20 +177,11 @@ export default React.createClass({
         if (this.props.tabComplete) {
             this.props.tabComplete.setTextArea(this.refs.textarea);
         }
-        window.addEventListener('beforeunload', this.onPageUnload);
     },
 
     componentWillUnmount: function() {
         dis.unregister(this.dispatcherRef);
         this.sentHistory.saveLastTextEntry();
-        window.removeEventListener('beforeunload', this.onPageUnload);
-    },
-
-    onPageUnload(event) {
-        if (this.isTyping) {
-            return event.returnValue =
-                'You seem to be typing a message, are you sure you want to quit?';
-        }
     },
 
     onAction: function(payload) {

From 356d29939c1deb2ce07694e653e0ee3c6d581d09 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 3 May 2017 16:25:27 +0100
Subject: [PATCH 29/73] also warn when quitting mid-call

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/RoomStatusBar.js | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index 0389b606aa..0cb246973b 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -100,6 +100,10 @@ module.exports = React.createClass({
         this._checkSize();
     },
 
+    componentDidMount: function() {
+        window.addEventListener('beforeunload', this.onPageUnload);
+    },
+
     componentDidUpdate: function() {
         this._checkSize();
     },
@@ -111,6 +115,7 @@ module.exports = React.createClass({
             client.removeListener("sync", this.onSyncStateChange);
             client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
         }
+        window.removeEventListener('beforeunload', this.onPageUnload);
     },
 
     onSyncStateChange: function(state, prevState) {
@@ -128,6 +133,13 @@ module.exports = React.createClass({
         });
     },
 
+    onPageUnload(event) {
+        if (this.props.hasActiveCall) {
+            return event.returnValue =
+                'You seem to be in a call, are you sure you want to quit?';
+        }
+    },
+
     // Check whether current size is greater than 0, if yes call props.onVisible
     _checkSize: function () {
         if (this.props.onVisible && this._getSize()) {

From 9d92f93fcbf7208bc2ee37f3bf2fcb8e15629e07 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 3 May 2017 16:36:57 +0100
Subject: [PATCH 30/73] consolidate call onPageUnload handler into RoomView

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/RoomStatusBar.js | 12 ------------
 src/components/structures/RoomView.js      |  3 +++
 2 files changed, 3 insertions(+), 12 deletions(-)

diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index 0cb246973b..0389b606aa 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -100,10 +100,6 @@ module.exports = React.createClass({
         this._checkSize();
     },
 
-    componentDidMount: function() {
-        window.addEventListener('beforeunload', this.onPageUnload);
-    },
-
     componentDidUpdate: function() {
         this._checkSize();
     },
@@ -115,7 +111,6 @@ module.exports = React.createClass({
             client.removeListener("sync", this.onSyncStateChange);
             client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
         }
-        window.removeEventListener('beforeunload', this.onPageUnload);
     },
 
     onSyncStateChange: function(state, prevState) {
@@ -133,13 +128,6 @@ module.exports = React.createClass({
         });
     },
 
-    onPageUnload(event) {
-        if (this.props.hasActiveCall) {
-            return event.returnValue =
-                'You seem to be in a call, are you sure you want to quit?';
-        }
-    },
-
     // Check whether current size is greater than 0, if yes call props.onVisible
     _checkSize: function () {
         if (this.props.onVisible && this._getSize()) {
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 9414a59e01..1b3ed6e80d 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -371,6 +371,9 @@ module.exports = React.createClass({
         if (ContentMessages.getCurrentUploads().length > 0) {
             return event.returnValue =
                 'You seem to be uploading files, are you sure you want to quit?';
+        } else if (this._getCallForRoom() && this.state.callState !== 'ended') {
+            return event.returnValue =
+                'You seem to be in a call, are you sure you want to quit?';
         }
     },
 

From bfa3123f9ba7f3aeab19c7205123c4639f7cd1ea Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Thu, 4 May 2017 10:00:13 +0100
Subject: [PATCH 31/73] Combine data-scroll-token and -contained-scroll-tokens

 - Instead of using one attribute, use one that might just contain one token
 - Use the first token when tracking a child
 - Mandate that no commas can be in individual tokens
---
 src/components/structures/MessagePanel.js     |  2 +-
 src/components/structures/ScrollPanel.js      | 41 ++++++++-----------
 .../views/elements/MemberEventListSummary.js  |  6 +--
 .../views/rooms/SearchResultTile.js           |  2 +-
 .../components/structures/ScrollPanel-test.js |  2 +-
 5 files changed, 24 insertions(+), 29 deletions(-)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 74aec61511..d4bf147ad5 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -472,7 +472,7 @@ module.exports = React.createClass({
         ret.push(
                 <li key={eventId}
                         ref={this._collectEventNode.bind(this, eventId)}
-                        data-scroll-token={scrollToken}>
+                        data-scroll-tokens={scrollToken}>
                     <EventTile mxEvent={mxEv} continuation={continuation}
                         isRedacted={mxEv.isRedacted()}
                         onWidgetLoad={this._onWidgetLoad}
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 3f36fac89b..50951312e7 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -46,13 +46,13 @@ if (DEBUG_SCROLL) {
  * It also provides a hook which allows parents to provide more list elements
  * when we get close to the start or end of the list.
  *
- * Each child element should have a 'data-scroll-token'. This token is used to
- * serialise the scroll state, and returned as the 'trackedScrollToken'
- * attribute by getScrollState().
+ * Each child element should have a 'data-scroll-tokens'. This string of
+ * comma-separated tokens may contain a single token or many, where many indicates
+ * that the element contains elements that have scroll tokens themselves. The first
+ * token in 'data-scroll-tokens' is used to serialise the scroll state, and returned
+ * as the 'trackedScrollToken' attribute by getScrollState().
  *
- * Child elements that contain elements that have scroll tokens must declare the
- * contained scroll tokens using 'data-contained-scroll-tokens`. When scrolling
- * to a contained scroll token, the ScrollPanel will scroll to the container.
+ * IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS.
  *
  * Some notes about the implementation:
  *
@@ -353,8 +353,8 @@ module.exports = React.createClass({
             // Subtract height of tile as if it were unpaginated
             excessHeight -= tile.clientHeight;
             // The tile may not have a scroll token, so guard it
-            if (tile.dataset.scrollToken) {
-                markerScrollToken = tile.dataset.scrollToken;
+            if (tile.dataset.scrollTokens) {
+                markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
             }
             if (tile.clientHeight > excessHeight) {
                 break;
@@ -423,7 +423,8 @@ module.exports = React.createClass({
      *   scroll. false if we are tracking a particular child.
      *
      * string trackedScrollToken: undefined if stuckAtBottom is true; if it is
-     *   false, the data-scroll-token of the child which we are tracking.
+     *   false, the fist token in data-scroll-tokens of the child which we are
+     *   tracking.
      *
      * number pixelOffset: undefined if stuckAtBottom is true; if it is false,
      *   the number of pixels the bottom of the tracked child is above the
@@ -555,16 +556,10 @@ module.exports = React.createClass({
         var messages = this.refs.itemlist.children;
         for (var i = messages.length-1; i >= 0; --i) {
             var m = messages[i];
-            // 'data-contained-scroll-tokens' has been set, indicating that a child
-            // element contains elements that each have a token. Check this array of
-            // tokens for `scrollToken`.
-            if (m.dataset.containedScrollTokens &&
-                m.dataset.containedScrollTokens.indexOf(scrollToken) !== -1) {
-                node = m;
-                break;
-            }
-            if (!m.dataset.scrollToken) continue;
-            if (m.dataset.scrollToken == scrollToken) {
+            // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
+            // There might only be one scroll token
+            if (m.dataset.scrollTokens &&
+                m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
                 node = m;
                 break;
             }
@@ -580,7 +575,7 @@ module.exports = React.createClass({
         var boundingRect = node.getBoundingClientRect();
         var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
 
-        debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
+        debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
                  pixelOffset + " (delta: "+scrollDelta+")");
 
         if(scrollDelta != 0) {
@@ -603,12 +598,12 @@ module.exports = React.createClass({
 
         for (var i = messages.length-1; i >= 0; --i) {
             var node = messages[i];
-            if (!node.dataset.scrollToken) continue;
+            if (!node.dataset.scrollTokens) continue;
 
             var boundingRect = node.getBoundingClientRect();
             newScrollState = {
                 stuckAtBottom: false,
-                trackedScrollToken: node.dataset.scrollToken,
+                trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
                 pixelOffset: wrapperRect.bottom - boundingRect.bottom,
             };
             // If the bottom of the panel intersects the ClientRect of node, use this node
@@ -620,7 +615,7 @@ module.exports = React.createClass({
                 break;
             }
         }
-        // This is only false if there were no nodes with `node.dataset.scrollToken` set.
+        // This is only false if there were no nodes with `node.dataset.scrollTokens` set.
         if (newScrollState) {
             this.scrollState = newScrollState;
             debuglog("ScrollPanel: saved scroll state", this.scrollState);
diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js
index a24e19577d..ae8678894d 100644
--- a/src/components/views/elements/MemberEventListSummary.js
+++ b/src/components/views/elements/MemberEventListSummary.js
@@ -369,7 +369,7 @@ module.exports = React.createClass({
 
     render: function() {
         const eventsToRender = this.props.events;
-        const eventIds = eventsToRender.map(e => e.getId());
+        const eventIds = eventsToRender.map(e => e.getId()).join(',');
         const fewEvents = eventsToRender.length < this.props.threshold;
         const expanded = this.state.expanded || fewEvents;
 
@@ -380,7 +380,7 @@ module.exports = React.createClass({
 
         if (fewEvents) {
             return (
-                <div className="mx_MemberEventListSummary" data-contained-scroll-tokens={eventIds}>
+                <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
                     {expandedEvents}
                 </div>
             );
@@ -438,7 +438,7 @@ module.exports = React.createClass({
         );
 
         return (
-            <div className="mx_MemberEventListSummary" data-contained-scroll-tokens={eventIds}>
+            <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
                 {toggleButton}
                 {summaryContainer}
                 {expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null}
diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js
index 7fac244481..1aba7c9196 100644
--- a/src/components/views/rooms/SearchResultTile.js
+++ b/src/components/views/rooms/SearchResultTile.js
@@ -60,7 +60,7 @@ module.exports = React.createClass({
             }
         }
         return (
-            <li data-scroll-token={eventId+"+"+j}>
+            <li data-scroll-tokens={eventId+"+"+j}>
                 {ret}
             </li>);
     },
diff --git a/test/components/structures/ScrollPanel-test.js b/test/components/structures/ScrollPanel-test.js
index eacaeb5fb4..7ecb74be6f 100644
--- a/test/components/structures/ScrollPanel-test.js
+++ b/test/components/structures/ScrollPanel-test.js
@@ -115,7 +115,7 @@ var Tester = React.createClass({
         //
         // there is an extra 50 pixels of margin at the bottom.
         return (
-            <li key={key} data-scroll-token={key}>
+            <li key={key} data-scroll-tokens={key}>
                 <div style={{height: '98px', margin: '50px', border: '1px solid black',
                              backgroundColor: '#fff8dc' }}>
                    {key}

From 6d9a1f047d1a2a82d3d0ab9596a30eafafb5c626 Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Thu, 4 May 2017 13:03:04 +0100
Subject: [PATCH 32/73] Typo

---
 src/components/structures/ScrollPanel.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 50951312e7..a652bcc827 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -423,7 +423,7 @@ module.exports = React.createClass({
      *   scroll. false if we are tracking a particular child.
      *
      * string trackedScrollToken: undefined if stuckAtBottom is true; if it is
-     *   false, the fist token in data-scroll-tokens of the child which we are
+     *   false, the first token in data-scroll-tokens of the child which we are
      *   tracking.
      *
      * number pixelOffset: undefined if stuckAtBottom is true; if it is false,

From ce119a63643309a22f31e004fe59c69ed37ab9a7 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 May 2017 13:55:52 +0100
Subject: [PATCH 33/73] Add buttons to room sub list headers

---
 src/component-index.js                      |  2 +
 src/components/views/elements/RoleButton.js | 75 +++++++++++++++++++++
 src/components/views/rooms/RoomList.js      | 41 +++++++----
 3 files changed, 105 insertions(+), 13 deletions(-)
 create mode 100644 src/components/views/elements/RoleButton.js

diff --git a/src/component-index.js b/src/component-index.js
index d6873c6dfd..68a734cbf6 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -125,6 +125,8 @@ import views$elements$PowerSelector from './components/views/elements/PowerSelec
 views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
 import views$elements$ProgressBar from './components/views/elements/ProgressBar';
 views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar);
+import views$elements$RoleButton from './components/views/elements/RoleButton';
+views$elements$RoleButton && (module.exports.components['views.elements.RoleButton'] = views$elements$RoleButton);
 import views$elements$TintableSvg from './components/views/elements/TintableSvg';
 views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
 import views$elements$TruncatedList from './components/views/elements/TruncatedList';
diff --git a/src/components/views/elements/RoleButton.js b/src/components/views/elements/RoleButton.js
new file mode 100644
index 0000000000..06006a5779
--- /dev/null
+++ b/src/components/views/elements/RoleButton.js
@@ -0,0 +1,75 @@
+/*
+Copyright Vector Creations 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.
+*/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import AccessibleButton from './AccessibleButton';
+import dis from '../../../dispatcher';
+import sdk from '../../../index';
+
+export default React.createClass({
+    displayName: 'RoleButton',
+
+    propTypes: {
+        role: PropTypes.string.isRequired,
+        size: PropTypes.string,
+    },
+
+    getDefaultProps: function() {
+        return {
+            size: 25,
+        };
+    },
+
+    _onClick: function(ev) {
+        ev.stopPropagation();
+
+        let action;
+        switch(this.props.role) {
+            case 'start_chat':
+                action = 'view_create_chat';
+                break;
+            case 'room_directory':
+                action = 'view_room_directory';
+                break;
+            case 'create_room':
+                action = 'view_create_room';
+                break;
+        }
+        if (action) dis.dispatch({action: action});
+    },
+
+    _getIconPath() {
+        switch(this.props.role) {
+            case 'start_chat':
+                return 'img/icons-people.svg';
+            case 'room_directory':
+                return 'img/icons-directory.svg';
+            case 'create_room':
+                return 'img/icons-create-room.svg';
+        }
+    },
+
+    render: function() {
+        const TintableSvg = sdk.getComponent("elements.TintableSvg");
+
+        return (
+            <AccessibleButton className="mx_RoleButton" onClick={ this._onClick }>
+                <TintableSvg src={this._getIconPath()} width={this.props.size} height={this.props.size} />
+            </AccessibleButton>
+        );
+    }
+});
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 963f5ad425..fe75271a25 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -1,5 +1,6 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
+Copyright Vector Creations Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -28,6 +29,7 @@ var Rooms = require('../../../Rooms');
 import DMRoomMap from '../../../utils/DMRoomMap';
 var Receipt = require('../../../utils/Receipt');
 var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
+import AccessibleButton from '../elements/AccessibleButton';
 
 const HIDE_CONFERENCE_CHANS = true;
 
@@ -617,27 +619,23 @@ module.exports = React.createClass({
     },
 
     _getEmptyContent: function(section) {
+        const RoleButton = sdk.getComponent('elements.RoleButton');
         if (this.state.totalRoomCount === 0) {
             const TintableSvg = sdk.getComponent('elements.TintableSvg');
             switch (section) {
                 case 'im.vector.fake.direct':
                     return <div className="mx_RoomList_emptySubListTip">
-                        <div className="mx_RoomList_butonPreview">
-                            <TintableSvg src="img/icons-people.svg" width="25" height="25" />
-                        </div>
-                        Use the button below to chat with someone!
+                        Press
+                        <RoleButton role='start_chat' size="16" />
+                        to start a chat with someone
                     </div>;
                 case 'im.vector.fake.recent':
                     return <div className="mx_RoomList_emptySubListTip">
-                        <div className="mx_RoomList_butonPreview">
-                            <TintableSvg src="img/icons-directory.svg" width="25" height="25" />
-                        </div>
-                        Use the button below to browse the room directory
-                        <br /><br />
-                        <div className="mx_RoomList_butonPreview">
-                            <TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
-                        </div>
-                        or this button to start a new one!
+                        You're not in any rooms yet! Press
+                        <RoleButton role='create_room' size="16" />
+                        to make a room or
+                        <RoleButton role='room_directory' size="16" />
+                        to browse the directory
                     </div>;
             }
         }
@@ -648,6 +646,21 @@ module.exports = React.createClass({
         return <RoomDropTarget label={labelText} />;
     },
 
+    _getHeaderItems: function(section) {
+        const RoleButton = sdk.getComponent('elements.RoleButton');
+        switch (section) {
+            case 'im.vector.fake.direct':
+                return <span className="mx_RoomList_headerButtons">
+                    <RoleButton role='start_chat' size="16" />
+                </span>;
+            case 'im.vector.fake.recent':
+                return <span className="mx_RoomList_headerButtons">
+                    <RoleButton role='room_directory' size="16" />
+                    <RoleButton role='create_room' size="16" />
+                </span>;
+        }
+    },
+
     render: function() {
         var RoomSubList = sdk.getComponent('structures.RoomSubList');
         var self = this;
@@ -685,6 +698,7 @@ module.exports = React.createClass({
                              label="People"
                              tagName="im.vector.fake.direct"
                              emptyContent={this._getEmptyContent('im.vector.fake.direct')}
+                             headerItems={this._getHeaderItems('im.vector.fake.direct')}
                              editable={ true }
                              order="recent"
                              incomingCall={ self.state.incomingCall }
@@ -700,6 +714,7 @@ module.exports = React.createClass({
                              tagName="im.vector.fake.recent"
                              editable={ true }
                              emptyContent={this._getEmptyContent('im.vector.fake.recent')}
+                             headerItems={this._getHeaderItems('im.vector.fake.recent')}
                              order="recent"
                              incomingCall={ self.state.incomingCall }
                              collapsed={ self.props.collapsed }

From 0998adb36693ed5dd445b360b2b86792b3c6912f Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 May 2017 15:02:21 +0100
Subject: [PATCH 34/73] What year is it? Who's the president?

---
 src/components/views/elements/RoleButton.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/RoleButton.js b/src/components/views/elements/RoleButton.js
index 06006a5779..f20b4c6b88 100644
--- a/src/components/views/elements/RoleButton.js
+++ b/src/components/views/elements/RoleButton.js
@@ -1,5 +1,5 @@
 /*
-Copyright Vector Creations Ltd
+Copyright 2017 Vector Creations Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From 6a1d0fbab54d10d6c06d5172f2e7d4d20e138b0f Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 May 2017 15:38:09 +0100
Subject: [PATCH 35/73] Make bottom left menu buttons use RoleButton too

---
 src/components/views/elements/RoleButton.js | 53 ++++++++++++++++++++-
 1 file changed, 52 insertions(+), 1 deletion(-)

diff --git a/src/components/views/elements/RoleButton.js b/src/components/views/elements/RoleButton.js
index f20b4c6b88..60f227a067 100644
--- a/src/components/views/elements/RoleButton.js
+++ b/src/components/views/elements/RoleButton.js
@@ -31,6 +31,13 @@ export default React.createClass({
     getDefaultProps: function() {
         return {
             size: 25,
+            tooltip: false,
+        };
+    },
+
+    getInitialState: function() {
+        return {
+            showTooltip: false,
         };
     },
 
@@ -48,10 +55,39 @@ export default React.createClass({
             case 'create_room':
                 action = 'view_create_room';
                 break;
+            case 'home_page':
+                action = 'view_home_page';
+                break;
+            case 'settings':
+                action = 'view_user_settings';
+                break;
         }
         if (action) dis.dispatch({action: action});
     },
 
+    _onMouseEnter: function() {
+        if (this.props.tooltip) this.setState({showTooltip: true});
+    },
+
+    _onMouseLeave: function() {
+        this.setState({showTooltip: false});
+    },
+
+    _getLabel() {
+        switch(this.props.role) {
+            case 'start_chat':
+                return 'Start chat';
+            case 'room_directory':
+                return 'Room directory';
+            case 'create_room':
+                return 'Create new room';
+            case 'home_page':
+                return 'Welcome page';
+            case 'settings':
+                return 'Settings';
+        }
+    },
+
     _getIconPath() {
         switch(this.props.role) {
             case 'start_chat':
@@ -60,15 +96,30 @@ export default React.createClass({
                 return 'img/icons-directory.svg';
             case 'create_room':
                 return 'img/icons-create-room.svg';
+            case 'home_page':
+                return 'img/icons-home.svg';
+            case 'settings':
+                return 'img/icons-settings.svg';
         }
     },
 
     render: function() {
         const TintableSvg = sdk.getComponent("elements.TintableSvg");
 
+        let tooltip;
+        if (this.state.showTooltip) {
+            const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
+            tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this._getLabel()} />;
+        }
+
         return (
-            <AccessibleButton className="mx_RoleButton" onClick={ this._onClick }>
+            <AccessibleButton className="mx_RoleButton"
+                onClick={this._onClick}
+                onMouseEnter={this._onMouseEnter}
+                onMouseLeave={this._onMouseLeave}
+            >
                 <TintableSvg src={this._getIconPath()} width={this.props.size} height={this.props.size} />
+                {tooltip}
             </AccessibleButton>
         );
     }

From 72df43d22b40884faed5155f289fae467235d205 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 May 2017 15:46:24 +0100
Subject: [PATCH 36/73] Year

---
 src/components/views/rooms/RoomList.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 0b56b87c72..33f6890a2a 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -1,6 +1,6 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
-Copyright Vector Creations Ltd
+Copyright 2017 Vector Creations Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From cb478f11941a1dd622e4a114aca8aa82cddaf302 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 4 May 2017 15:50:52 +0100
Subject: [PATCH 37/73] no idea why those got in there...

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/UserSettings.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 4b63c8d888..098d370d65 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -628,8 +628,8 @@ module.exports = React.createClass({
                 <h3>Cryptography</h3>
                 <div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
                     <ul>
-                        <li><label>Device ID:</label>                       <span><code>{deviceId}</code></span></li>
-                        <li><label>Device key:</label>                      <span><code><b>{identityKey}</b></code></span></li>
+                        <li><label>Device ID:</label>             <span><code>{deviceId}</code></span></li>
+                        <li><label>Device key:</label>            <span><code><b>{identityKey}</b></code></span></li>
                     </ul>
                     { importExportButtons }
                 </div>

From 2edfc3e5985ca271992edc83b3d1d6ad3179b8d6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 4 May 2017 15:51:31 +0100
Subject: [PATCH 38/73] remove commented out code as per review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/UserSettings.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 098d370d65..7f6a5f329c 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -538,8 +538,6 @@ module.exports = React.createClass({
     },
 
     _renderUserInterfaceSettings: function() {
-        // const client = MatrixClientPeg.get();
-
         return (
             <div>
                 <h3>User Interface</h3>

From 909cdb6e9aadfb115f38d859a82e0583daa9b114 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 May 2017 16:22:06 +0100
Subject: [PATCH 39/73] Depend on prop-types module

So we can start writing code compatible with new React
---
 package.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/package.json b/package.json
index 95a82bbb73..37903252c4 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
     "lodash": "^4.13.1",
     "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
     "optimist": "^0.6.1",
+    "prop-types": "^15.5.8",
     "q": "^1.4.1",
     "react": "^15.4.0",
     "react-addons-css-transition-group": "15.3.2",

From 3c6e301f7fdc4906f2d388ef8e6bd3254434cc48 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 4 May 2017 16:22:39 +0100
Subject: [PATCH 40/73] Improve regex to ignore trailing -dirty and for rc tags

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/UserSettings.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 7f6a5f329c..46dce8bd2e 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -35,10 +35,10 @@ import AccessibleButton from '../views/elements/AccessibleButton';
 const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
 
 // Simple method to help prettify GH Release Tags and Commit Hashes.
-const semVerRegex = /^v?(\d+\.\d+\.\d+)(?:-(?:\d+-g)?(.+))?|$/i;
+const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
 const gHVersionLabel = function(repo, token) {
     const match = token.match(semVerRegex);
-    let url; // assume commit hash
+    let url;
     if (match && match[1]) { // basic semVer string possibly with commit hash
         url = (match.length > 1 && match[2])
             ? `https://github.com/${repo}/commit/${match[2]}`

From da4c2f8b31aeba163f50d9a1c8570bce2b052eaa Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Thu, 4 May 2017 16:42:41 +0100
Subject: [PATCH 41/73] Guests can't send RR

so they shouldn't try
lets not hit the HS quite as much
---
 src/components/structures/TimelinePanel.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index f72d35c41c..7bf89973c8 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -503,7 +503,9 @@ var TimelinePanel = React.createClass({
         // This happens on user_activity_end which is delayed, and it's
         // very possible have logged out within that timeframe, so check
         // we still have a client.
-        if (!MatrixClientPeg.get()) return;
+        const cli = MatrixClientPeg.get();
+        // if no client or client is guest don't send RR (vector-im/riot-web#3758)
+        if (!cli || cli.isGuest()) return;
 
         var currentReadUpToEventId = this._getCurrentReadReceipt(true);
         var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);

From 396b38512c8c1b9cea7e217910e7cad8730fd5da Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 May 2017 16:50:47 +0100
Subject: [PATCH 42/73] Remove babelcheck

Nobody is likley to be upgrading from babel 5 any more, so this
can go away now.
---
 package.json          |  4 ++--
 scripts/babelcheck.js | 22 ----------------------
 2 files changed, 2 insertions(+), 24 deletions(-)
 delete mode 100644 scripts/babelcheck.js

diff --git a/package.json b/package.json
index 95a82bbb73..836f7fd353 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,8 @@
   },
   "scripts": {
     "reskindex": "scripts/reskindex.js -h header",
-    "build": "node scripts/babelcheck.js && babel src -d lib --source-maps",
-    "start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps",
+    "build": "babel src -d lib --source-maps",
+    "start": "babel src -w -d lib --source-maps",
     "lint": "eslint src/",
     "lintall": "eslint src/ test/",
     "clean": "rimraf lib",
diff --git a/scripts/babelcheck.js b/scripts/babelcheck.js
deleted file mode 100644
index 14e4a28a70..0000000000
--- a/scripts/babelcheck.js
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env node
-
-var exec = require('child_process').exec;
-
-// Makes sure the babel executable in the path is babel 6 (or greater), not
-// babel 5, which it is if you upgrade from an older version of react-sdk and
-// run 'npm install' since the package has changed to babel-cli, so 'babel'
-// remains installed and the executable in node_modules/.bin remains as babel
-// 5.
-
-exec("babel -V", function (error, stdout, stderr) {
-    if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) {
-        console.log("\033[31m\033[1m"+
-            '*****************************************\n'+
-            '* matrix-react-sdk has moved to babel 6 *\n'+
-            '* Please "rm -rf node_modules && npm i" *\n'+
-            '* then restore links as appropriate     *\n'+
-            '*****************************************\n'+
-        "\033[91m");
-        process.exit(1);
-    }
-});

From f76b9b44897dbf54f94052bc561e9147967d90c7 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Thu, 4 May 2017 17:25:23 +0100
Subject: [PATCH 43/73] remove link to issue

not very useful
---
 src/components/structures/TimelinePanel.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 7bf89973c8..d29a08ceb4 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -504,7 +504,7 @@ var TimelinePanel = React.createClass({
         // very possible have logged out within that timeframe, so check
         // we still have a client.
         const cli = MatrixClientPeg.get();
-        // if no client or client is guest don't send RR (vector-im/riot-web#3758)
+        // if no client or client is guest don't send RR
         if (!cli || cli.isGuest()) return;
 
         var currentReadUpToEventId = this._getCurrentReadReceipt(true);

From 8d4663f43714d8f54a127f12d7fd02620f3c892b Mon Sep 17 00:00:00 2001
From: Richard van der Hoff <richard@matrix.org>
Date: Thu, 4 May 2017 18:03:35 +0100
Subject: [PATCH 44/73] Fix lint in Lifecycle.js

---
 src/Lifecycle.js | 47 ++++++++++++++++++++++++-----------------------
 1 file changed, 24 insertions(+), 23 deletions(-)

diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index f20716cae6..e3be318b31 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -49,7 +49,7 @@ import sdk from './index';
  * If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
  * turn will raise on_logged_in and will_start_client events.
  *
- * It returns a promise which resolves when the above process completes.
+ * @param {object} opts
  *
  * @param {object} opts.realQueryParams: string->string map of the
  *     query-parameters extracted from the real query-string of the starting
@@ -67,6 +67,7 @@ import sdk from './index';
  * @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
  *     true; defines the IS to use.
  *
+ * @returns {Promise} a promise which resolves when the above process completes.
  */
 export function loadSession(opts) {
     const realQueryParams = opts.realQueryParams || {};
@@ -127,7 +128,7 @@ export function loadSession(opts) {
 
 function _loginWithToken(queryParams, defaultDeviceDisplayName) {
     // create a temporary MatrixClient to do the login
-    var client = Matrix.createClient({
+    const client = Matrix.createClient({
         baseUrl: queryParams.homeserver,
     });
 
@@ -159,7 +160,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
     // Not really sure where the right home for it is.
 
     // create a temporary MatrixClient to do the login
-    var client = Matrix.createClient({
+    const client = Matrix.createClient({
         baseUrl: hsUrl,
     });
 
@@ -188,30 +189,30 @@ function _restoreFromLocalStorage() {
     if (!localStorage) {
         return q(false);
     }
-    const hs_url = localStorage.getItem("mx_hs_url");
-    const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
-    const access_token = localStorage.getItem("mx_access_token");
-    const user_id = localStorage.getItem("mx_user_id");
-    const device_id = localStorage.getItem("mx_device_id");
+    const hsUrl = localStorage.getItem("mx_hs_url");
+    const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
+    const accessToken = localStorage.getItem("mx_access_token");
+    const userId = localStorage.getItem("mx_user_id");
+    const deviceId = localStorage.getItem("mx_device_id");
 
-    let is_guest;
+    let isGuest;
     if (localStorage.getItem("mx_is_guest") !== null) {
-        is_guest = localStorage.getItem("mx_is_guest") === "true";
+        isGuest = localStorage.getItem("mx_is_guest") === "true";
     } else {
         // legacy key name
-        is_guest = localStorage.getItem("matrix-is-guest") === "true";
+        isGuest = localStorage.getItem("matrix-is-guest") === "true";
     }
 
-    if (access_token && user_id && hs_url) {
-        console.log("Restoring session for %s", user_id);
+    if (accessToken && userId && hsUrl) {
+        console.log("Restoring session for %s", userId);
         try {
             setLoggedIn({
-                userId: user_id,
-                deviceId: device_id,
-                accessToken: access_token,
-                homeserverUrl: hs_url,
-                identityServerUrl: is_url,
-                guest: is_guest,
+                userId: userId,
+                deviceId: deviceId,
+                accessToken: accessToken,
+                homeserverUrl: hsUrl,
+                identityServerUrl: isUrl,
+                guest: isGuest,
             });
             return q(true);
         } catch (e) {
@@ -352,7 +353,7 @@ export function logout() {
         return;
     }
 
-    return MatrixClientPeg.get().logout().then(onLoggedOut,
+    MatrixClientPeg.get().logout().then(onLoggedOut,
         (err) => {
             // Just throwing an error here is going to be very unhelpful
             // if you're trying to log out because your server's down and
@@ -363,8 +364,8 @@ export function logout() {
             // change your password).
             console.log("Failed to call logout API: token will not be invalidated");
             onLoggedOut();
-        }
-    );
+        },
+    ).done();
 }
 
 /**
@@ -420,7 +421,7 @@ export function stopMatrixClient() {
     UserActivity.stop();
     Presence.stop();
     if (DMRoomMap.shared()) DMRoomMap.shared().stop();
-    var cli = MatrixClientPeg.get();
+    const cli = MatrixClientPeg.get();
     if (cli) {
         cli.stopClient();
         cli.removeAllListeners();

From bfceaa827b4053894f67c74d5112f0a482751c69 Mon Sep 17 00:00:00 2001
From: Richard van der Hoff <richard@matrix.org>
Date: Thu, 4 May 2017 18:04:47 +0100
Subject: [PATCH 45/73] Log deviceid at login

- to help understand rageshakes
---
 src/Lifecycle.js | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index e3be318b31..f34aeae0e5 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -274,9 +274,13 @@ export function initRtsClient(url) {
  */
 export function setLoggedIn(credentials) {
     credentials.guest = Boolean(credentials.guest);
-    console.log("setLoggedIn => %s (guest=%s) hs=%s",
-                credentials.userId, credentials.guest,
-                credentials.homeserverUrl);
+
+    console.log(
+        "setLoggedIn: mxid:", credentials.userId,
+        "deviceId:", credentials.deviceId,
+        "guest:", credentials.guest,
+        "hs:", credentials.homeserverUrl,
+    );
     // This is dispatched to indicate that the user is still in the process of logging in
     // because `teamPromise` may take some time to resolve, breaking the assumption that
     // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms

From f86ca5bc97e915ae097f5e4eba459aba9e0973b8 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 May 2017 18:08:04 +0100
Subject: [PATCH 46/73] Hide empty tips if collapsed

---
 src/components/views/rooms/RoomList.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 33f6890a2a..6f5097840f 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -578,6 +578,12 @@ module.exports = React.createClass({
     },
 
     _getEmptyContent: function(section) {
+        const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
+
+        if (this.props.collapsed) {
+            return <RoomDropTarget label="" />;
+        }
+
         const RoleButton = sdk.getComponent('elements.RoleButton');
         if (this.state.totalRoomCount === 0) {
             const TintableSvg = sdk.getComponent('elements.TintableSvg');
@@ -598,7 +604,6 @@ module.exports = React.createClass({
                     </div>;
             }
         }
-        const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
 
         const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
 

From e00605571bc9ef68ff9f2cec33b06ee7213ae457 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 5 May 2017 10:48:54 +0100
Subject: [PATCH 47/73] Fix the spinner to actually appear

We started with clientSyncState being null, which it remained
until the SYNCING event was emitted. We need to set
clientSyncState's initial value correctly.
---
 src/components/structures/TimelinePanel.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index d29a08ceb4..7c89694a29 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -170,7 +170,7 @@ var TimelinePanel = React.createClass({
             forwardPaginating: false,
 
             // cache of matrixClient.getSyncState() (but from the 'sync' event)
-            clientSyncState: null,
+            clientSyncState: MatrixClientPeg.get().getSyncState(),
         };
     },
 

From 4a5821e1999748a9fae288a08f0def9e4a72f897 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 5 May 2017 14:25:18 +0100
Subject: [PATCH 48/73] Separate classes for the different buttons

Also rename RoleButton to ActionButton because it's not being
given a Role any more.
---
 src/component-index.js                        | 14 ++++-
 .../{RoleButton.js => ActionButton.js}        | 60 +++----------------
 .../views/elements/CreateRoomButton.js        | 37 ++++++++++++
 src/components/views/elements/HomeButton.js   | 37 ++++++++++++
 .../views/elements/RoomDirectoryButton.js     | 37 ++++++++++++
 .../views/elements/SettingsButton.js          | 37 ++++++++++++
 .../views/elements/StartChatButton.js         | 37 ++++++++++++
 src/components/views/rooms/RoomList.js        | 20 ++++---
 8 files changed, 216 insertions(+), 63 deletions(-)
 rename src/components/views/elements/{RoleButton.js => ActionButton.js} (54%)
 create mode 100644 src/components/views/elements/CreateRoomButton.js
 create mode 100644 src/components/views/elements/HomeButton.js
 create mode 100644 src/components/views/elements/RoomDirectoryButton.js
 create mode 100644 src/components/views/elements/SettingsButton.js
 create mode 100644 src/components/views/elements/StartChatButton.js

diff --git a/src/component-index.js b/src/component-index.js
index 68a734cbf6..090a27d5ed 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -103,10 +103,14 @@ import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/Unknow
 views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
 import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
 views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
+import views$elements$ActionButton from './components/views/elements/ActionButton';
+views$elements$ActionButton && (module.exports.components['views.elements.ActionButton'] = views$elements$ActionButton);
 import views$elements$AddressSelector from './components/views/elements/AddressSelector';
 views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
 import views$elements$AddressTile from './components/views/elements/AddressTile';
 views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile);
+import views$elements$CreateRoomButton from './components/views/elements/CreateRoomButton';
+views$elements$CreateRoomButton && (module.exports.components['views.elements.CreateRoomButton'] = views$elements$CreateRoomButton);
 import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons';
 views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
 import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
@@ -119,14 +123,20 @@ import views$elements$EditableTextContainer from './components/views/elements/Ed
 views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
 import views$elements$EmojiText from './components/views/elements/EmojiText';
 views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText);
+import views$elements$HomeButton from './components/views/elements/HomeButton';
+views$elements$HomeButton && (module.exports.components['views.elements.HomeButton'] = views$elements$HomeButton);
 import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary';
 views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
 import views$elements$PowerSelector from './components/views/elements/PowerSelector';
 views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
 import views$elements$ProgressBar from './components/views/elements/ProgressBar';
 views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar);
-import views$elements$RoleButton from './components/views/elements/RoleButton';
-views$elements$RoleButton && (module.exports.components['views.elements.RoleButton'] = views$elements$RoleButton);
+import views$elements$RoomDirectoryButton from './components/views/elements/RoomDirectoryButton';
+views$elements$RoomDirectoryButton && (module.exports.components['views.elements.RoomDirectoryButton'] = views$elements$RoomDirectoryButton);
+import views$elements$SettingsButton from './components/views/elements/SettingsButton';
+views$elements$SettingsButton && (module.exports.components['views.elements.SettingsButton'] = views$elements$SettingsButton);
+import views$elements$StartChatButton from './components/views/elements/StartChatButton';
+views$elements$StartChatButton && (module.exports.components['views.elements.StartChatButton'] = views$elements$StartChatButton);
 import views$elements$TintableSvg from './components/views/elements/TintableSvg';
 views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
 import views$elements$TruncatedList from './components/views/elements/TruncatedList';
diff --git a/src/components/views/elements/RoleButton.js b/src/components/views/elements/ActionButton.js
similarity index 54%
rename from src/components/views/elements/RoleButton.js
rename to src/components/views/elements/ActionButton.js
index 60f227a067..6d6289ddab 100644
--- a/src/components/views/elements/RoleButton.js
+++ b/src/components/views/elements/ActionButton.js
@@ -24,8 +24,11 @@ export default React.createClass({
     displayName: 'RoleButton',
 
     propTypes: {
-        role: PropTypes.string.isRequired,
         size: PropTypes.string,
+        tooltip: PropTypes.bool,
+        action: PropTypes.string.isRequired,
+        label: PropTypes.string.isRequired,
+        iconPath: PropTypes.string.isRequired,
     },
 
     getDefaultProps: function() {
@@ -43,26 +46,7 @@ export default React.createClass({
 
     _onClick: function(ev) {
         ev.stopPropagation();
-
-        let action;
-        switch(this.props.role) {
-            case 'start_chat':
-                action = 'view_create_chat';
-                break;
-            case 'room_directory':
-                action = 'view_room_directory';
-                break;
-            case 'create_room':
-                action = 'view_create_room';
-                break;
-            case 'home_page':
-                action = 'view_home_page';
-                break;
-            case 'settings':
-                action = 'view_user_settings';
-                break;
-        }
-        if (action) dis.dispatch({action: action});
+        dis.dispatch({action: this.props.action});
     },
 
     _onMouseEnter: function() {
@@ -73,43 +57,13 @@ export default React.createClass({
         this.setState({showTooltip: false});
     },
 
-    _getLabel() {
-        switch(this.props.role) {
-            case 'start_chat':
-                return 'Start chat';
-            case 'room_directory':
-                return 'Room directory';
-            case 'create_room':
-                return 'Create new room';
-            case 'home_page':
-                return 'Welcome page';
-            case 'settings':
-                return 'Settings';
-        }
-    },
-
-    _getIconPath() {
-        switch(this.props.role) {
-            case 'start_chat':
-                return 'img/icons-people.svg';
-            case 'room_directory':
-                return 'img/icons-directory.svg';
-            case 'create_room':
-                return 'img/icons-create-room.svg';
-            case 'home_page':
-                return 'img/icons-home.svg';
-            case 'settings':
-                return 'img/icons-settings.svg';
-        }
-    },
-
     render: function() {
         const TintableSvg = sdk.getComponent("elements.TintableSvg");
 
         let tooltip;
         if (this.state.showTooltip) {
             const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
-            tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this._getLabel()} />;
+            tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
         }
 
         return (
@@ -118,7 +72,7 @@ export default React.createClass({
                 onMouseEnter={this._onMouseEnter}
                 onMouseLeave={this._onMouseLeave}
             >
-                <TintableSvg src={this._getIconPath()} width={this.props.size} height={this.props.size} />
+                <TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
                 {tooltip}
             </AccessibleButton>
         );
diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js
new file mode 100644
index 0000000000..d6b6526d6c
--- /dev/null
+++ b/src/components/views/elements/CreateRoomButton.js
@@ -0,0 +1,37 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const CreateRoomButton = function(props) {
+    const ActionButton = sdk.getComponent('elements.ActionButton');
+    return (
+        <ActionButton action="view_create_chat"
+            label="Create new room"
+            iconPath="img/icons-create-room.svg"
+            size={props.size}
+            tooltip={props.tooltip}
+        />
+    );
+};
+
+CreateRoomButton.propTypes = {
+    size: PropTypes.string,
+    tooltip: PropTypes.bool,
+};
+
+export default CreateRoomButton;
diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js
new file mode 100644
index 0000000000..4c7f295c87
--- /dev/null
+++ b/src/components/views/elements/HomeButton.js
@@ -0,0 +1,37 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const HomeButton = function(props) {
+    const ActionButton = sdk.getComponent('elements.ActionButton');
+    return (
+        <ActionButton action="view_home_page"
+            label="Welcome page"
+            iconPath="img/icons-home.svg"
+            size={props.size}
+            tooltip={props.tooltip}
+        />
+    );
+};
+
+HomeButton.propTypes = {
+    size: PropTypes.string,
+    tooltip: PropTypes.bool,
+};
+
+export default HomeButton;
diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js
new file mode 100644
index 0000000000..651dd8edd0
--- /dev/null
+++ b/src/components/views/elements/RoomDirectoryButton.js
@@ -0,0 +1,37 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const RoomDirectoryButton = function(props) {
+    const ActionButton = sdk.getComponent('elements.ActionButton');
+    return (
+        <ActionButton action="view_room_directory"
+            label="Room directory"
+            iconPath="img/icons-directory.svg"
+            size={props.size}
+            tooltip={props.tooltip}
+        />
+    );
+};
+
+RoomDirectoryButton.propTypes = {
+    size: PropTypes.string,
+    tooltip: PropTypes.bool,
+};
+
+export default RoomDirectoryButton;
diff --git a/src/components/views/elements/SettingsButton.js b/src/components/views/elements/SettingsButton.js
new file mode 100644
index 0000000000..51da6e3fd1
--- /dev/null
+++ b/src/components/views/elements/SettingsButton.js
@@ -0,0 +1,37 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const SettingsButton = function(props) {
+    const ActionButton = sdk.getComponent('elements.ActionButton');
+    return (
+        <ActionButton action="view_user_settings"
+            label="Settings"
+            iconPath="img/icons-settings.svg"
+            size={props.size}
+            tooltip={props.tooltip}
+        />
+    );
+};
+
+SettingsButton.propTypes = {
+    size: PropTypes.string,
+    tooltip: PropTypes.bool,
+};
+
+export default SettingsButton;
diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js
new file mode 100644
index 0000000000..66cd911754
--- /dev/null
+++ b/src/components/views/elements/StartChatButton.js
@@ -0,0 +1,37 @@
+/*
+Copyright 2017 Vector Creations 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.
+*/
+
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+
+const StartChatButton = function(props) {
+    const ActionButton = sdk.getComponent('elements.ActionButton');
+    return (
+        <ActionButton action="start_chat"
+            label="Start chat"
+            iconPath="img/icons-people.svg"
+            size={props.size}
+            tooltip={props.tooltip}
+        />
+    );
+};
+
+StartChatButton.propTypes = {
+    size: PropTypes.string,
+    tooltip: PropTypes.bool,
+};
+
+export default StartChatButton;
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 6f5097840f..5917af5eb1 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -584,22 +584,24 @@ module.exports = React.createClass({
             return <RoomDropTarget label="" />;
         }
 
-        const RoleButton = sdk.getComponent('elements.RoleButton');
+        const StartChatButton = sdk.getComponent('elements.StartChatButton');
+        const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
+        const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
         if (this.state.totalRoomCount === 0) {
             const TintableSvg = sdk.getComponent('elements.TintableSvg');
             switch (section) {
                 case 'im.vector.fake.direct':
                     return <div className="mx_RoomList_emptySubListTip">
                         Press
-                        <RoleButton role='start_chat' size="16" />
+                        <StartChatButton size="16" />
                         to start a chat with someone
                     </div>;
                 case 'im.vector.fake.recent':
                     return <div className="mx_RoomList_emptySubListTip">
                         You're not in any rooms yet! Press
-                        <RoleButton role='create_room' size="16" />
+                        <CreateRoomButton size="16" />
                         to make a room or
-                        <RoleButton role='room_directory' size="16" />
+                        <RoomDirectoryButton size="16" />
                         to browse the directory
                     </div>;
             }
@@ -611,16 +613,18 @@ module.exports = React.createClass({
     },
 
     _getHeaderItems: function(section) {
-        const RoleButton = sdk.getComponent('elements.RoleButton');
+        const StartChatButton = sdk.getComponent('elements.StartChatButton');
+        const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
+        const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
         switch (section) {
             case 'im.vector.fake.direct':
                 return <span className="mx_RoomList_headerButtons">
-                    <RoleButton role='start_chat' size="16" />
+                    <StartChatButton role='start_chat' size="16" />
                 </span>;
             case 'im.vector.fake.recent':
                 return <span className="mx_RoomList_headerButtons">
-                    <RoleButton role='room_directory' size="16" />
-                    <RoleButton role='create_room' size="16" />
+                    <RoomDirectoryButton role='room_directory' size="16" />
+                    <CreateRoomButton role='create_room' size="16" />
                 </span>;
         }
     },

From 4fc4ae1e9907267d97d96ac9c5ac31b6140aac6b Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 5 May 2017 14:56:26 +0100
Subject: [PATCH 49/73] Size is a string, import react

React gets put in by the JSX transpile
---
 src/components/views/elements/ActionButton.js        | 2 +-
 src/components/views/elements/CreateRoomButton.js    | 1 +
 src/components/views/elements/HomeButton.js          | 1 +
 src/components/views/elements/RoomDirectoryButton.js | 1 +
 src/components/views/elements/SettingsButton.js      | 1 +
 src/components/views/elements/StartChatButton.js     | 1 +
 6 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js
index 6d6289ddab..267388daf6 100644
--- a/src/components/views/elements/ActionButton.js
+++ b/src/components/views/elements/ActionButton.js
@@ -33,7 +33,7 @@ export default React.createClass({
 
     getDefaultProps: function() {
         return {
-            size: 25,
+            size: "25",
             tooltip: false,
         };
     },
diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js
index d6b6526d6c..e7e526d36b 100644
--- a/src/components/views/elements/CreateRoomButton.js
+++ b/src/components/views/elements/CreateRoomButton.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import React from 'react';
 import sdk from '../../../index';
 import PropTypes from 'prop-types';
 
diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js
index 4c7f295c87..5c446f24c9 100644
--- a/src/components/views/elements/HomeButton.js
+++ b/src/components/views/elements/HomeButton.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import React from 'react';
 import sdk from '../../../index';
 import PropTypes from 'prop-types';
 
diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js
index 651dd8edd0..5e68776a15 100644
--- a/src/components/views/elements/RoomDirectoryButton.js
+++ b/src/components/views/elements/RoomDirectoryButton.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import React from 'react';
 import sdk from '../../../index';
 import PropTypes from 'prop-types';
 
diff --git a/src/components/views/elements/SettingsButton.js b/src/components/views/elements/SettingsButton.js
index 51da6e3fd1..c6438da277 100644
--- a/src/components/views/elements/SettingsButton.js
+++ b/src/components/views/elements/SettingsButton.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import React from 'react';
 import sdk from '../../../index';
 import PropTypes from 'prop-types';
 
diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js
index 66cd911754..02d5677a7c 100644
--- a/src/components/views/elements/StartChatButton.js
+++ b/src/components/views/elements/StartChatButton.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import React from 'react';
 import sdk from '../../../index';
 import PropTypes from 'prop-types';
 

From 1a0ea29995d38b5ef824b11aa299b98cea12ebd3 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 5 May 2017 17:51:14 +0100
Subject: [PATCH 50/73] Remove redundant role elements

---
 src/components/views/rooms/RoomList.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 5917af5eb1..8d396b5536 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -619,12 +619,12 @@ module.exports = React.createClass({
         switch (section) {
             case 'im.vector.fake.direct':
                 return <span className="mx_RoomList_headerButtons">
-                    <StartChatButton role='start_chat' size="16" />
+                    <StartChatButton size="16" />
                 </span>;
             case 'im.vector.fake.recent':
                 return <span className="mx_RoomList_headerButtons">
-                    <RoomDirectoryButton role='room_directory' size="16" />
-                    <CreateRoomButton role='create_room' size="16" />
+                    <RoomDirectoryButton size="16" />
+                    <CreateRoomButton size="16" />
                 </span>;
         }
     },

From 2b71123ddcacb103d2338d8d525fd4922d24ff68 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Sat, 6 May 2017 01:45:28 +0100
Subject: [PATCH 51/73] suppress null member rejoins again

reverts https://github.com/matrix-org/matrix-react-sdk/commit/f5fe4b24336a8245b5a08050fefe13b9999d92fb
should fix https://github.com/vector-im/riot-web/issues/3788
---
 src/TextForEvent.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 40d6a49998..3f200a089d 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -65,8 +65,8 @@ function textForMemberEvent(ev) {
                 } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
                     return senderName + " set a profile picture";
                 } else {
-                    // hacky hack for https://github.com/vector-im/vector-web/issues/2020
-                    return senderName + " rejoined the room.";
+                    // suppress null rejoins
+                    return '';
                 }
             } else {
                 if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);

From 2b2b43a7f3d57dcbf53a70817b31d6287974e517 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 7 May 2017 18:15:37 +0100
Subject: [PATCH 52/73] Content in Composer is not lost on unload so it should
 be fine to scare the user thinking they have lost all of their content even
 though when they come back they can cry with joy :D

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 2161198142..88230062fe 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -43,7 +43,6 @@ export default class MessageComposer extends React.Component {
         this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
         this.onInputStateChanged = this.onInputStateChanged.bind(this);
         this.onEvent = this.onEvent.bind(this);
-        this.onPageUnload = this.onPageUnload.bind(this);
 
         this.state = {
             autocompleteQuery: '',
@@ -65,22 +64,12 @@ export default class MessageComposer extends React.Component {
         // marked as encrypted.
         // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
         MatrixClientPeg.get().on("event", this.onEvent);
-
-        window.addEventListener('beforeunload', this.onPageUnload);
     }
 
     componentWillUnmount() {
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener("event", this.onEvent);
         }
-        window.removeEventListener('beforeunload', this.onPageUnload);
-    }
-
-    onPageUnload(event) {
-        if (this.messageComposerInput && this.messageComposerInput.isTyping) {
-            return event.returnValue =
-                'You seem to be typing a message, are you sure you want to quit?';
-        }
     }
 
     onEvent(event) {

From a14135067005f7d0e07121dcefb29f548c6754da Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 7 May 2017 20:01:55 +0100
Subject: [PATCH 53/73] Explicitly save composer content onUnload small
 oversight, caught by Matthew

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 88230062fe..0ee3c2082d 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -43,6 +43,7 @@ export default class MessageComposer extends React.Component {
         this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
         this.onInputStateChanged = this.onInputStateChanged.bind(this);
         this.onEvent = this.onEvent.bind(this);
+        this.onPageUnload = this.onPageUnload.bind(this);
 
         this.state = {
             autocompleteQuery: '',
@@ -64,12 +65,21 @@ export default class MessageComposer extends React.Component {
         // marked as encrypted.
         // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
         MatrixClientPeg.get().on("event", this.onEvent);
+
+        window.addEventListener('beforeunload', this.onPageUnload);
     }
 
     componentWillUnmount() {
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener("event", this.onEvent);
         }
+        window.removeEventListener('beforeunload', this.onPageUnload);
+    }
+
+    onPageUnload(event) {
+        if (this.messageComposerInput) {
+            this.messageComposerInput.sentHistory.saveLastTextEntry();
+        }
     }
 
     onEvent(event) {

From 78e72723440780814fced82cc8a2406c47992464 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Sun, 7 May 2017 20:43:42 +0100
Subject: [PATCH 54/73] Fixes 2 issues with Dialog closing

+ Upload Confirmation dialog would just change focus on ESC and not close
+ Keywords Dialog in UserSettings would also close UserSettings because event bubbled up
---
 src/components/views/dialogs/BaseDialog.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index 279dedbd43..d567a0ba9a 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -57,6 +57,12 @@ export default React.createClass({
         }
     },
 
+    // Don't let key down events get any further, so they only trigger this and nothing more
+    _onKeyDown: function(e) {
+        e.stopPropagation();
+        e.preventDefault();
+    },
+    
     // Must be when the key is released (and not pressed) otherwise componentWillUnmount
     // will focus another element which will receive future key events
     _onKeyUp: function(e) {
@@ -81,7 +87,7 @@ export default React.createClass({
         const TintableSvg = sdk.getComponent("elements.TintableSvg");
 
         return (
-            <div onKeyUp={this._onKeyUp} className={this.props.className}>
+            <div onKeyDown={this._onKeyDown} onKeyUp={this._onKeyUp} className={this.props.className}>
                 <AccessibleButton onClick={this._onCancelClick}
                     className="mx_Dialog_cancelButton"
                 >

From 360f1cd250378a54d0305464111b2af57fb79731 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Sun, 7 May 2017 20:57:54 +0100
Subject: [PATCH 55/73] completely missed the ESC check

I need sleep
---
 src/components/views/dialogs/BaseDialog.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index d567a0ba9a..e7b8a687f5 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -59,8 +59,10 @@ export default React.createClass({
 
     // Don't let key down events get any further, so they only trigger this and nothing more
     _onKeyDown: function(e) {
-        e.stopPropagation();
-        e.preventDefault();
+        if (e.keyCode === KeyCode.ESCAPE) {
+            e.stopPropagation();
+            e.preventDefault();
+        }
     },
     
     // Must be when the key is released (and not pressed) otherwise componentWillUnmount

From bd32df4ef6dbf66f1ba5d9b23117fefbe2132c72 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Sun, 7 May 2017 20:58:30 +0100
Subject: [PATCH 56/73] comment wording

---
 src/components/views/dialogs/BaseDialog.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index e7b8a687f5..ac36dfd056 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -57,7 +57,7 @@ export default React.createClass({
         }
     },
 
-    // Don't let key down events get any further, so they only trigger this and nothing more
+    // Don't let esc keydown events get any further, so they only trigger this and nothing more
     _onKeyDown: function(e) {
         if (e.keyCode === KeyCode.ESCAPE) {
             e.stopPropagation();

From c841eb641b85995bddb235d3db2c779daffe97a1 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 8 May 2017 11:26:25 +0100
Subject: [PATCH 57/73] Fix 'start chat' button

---
 src/components/views/elements/StartChatButton.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js
index 02d5677a7c..747f75d1b3 100644
--- a/src/components/views/elements/StartChatButton.js
+++ b/src/components/views/elements/StartChatButton.js
@@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
 const StartChatButton = function(props) {
     const ActionButton = sdk.getComponent('elements.ActionButton');
     return (
-        <ActionButton action="start_chat"
+        <ActionButton action="view_create_chat"
             label="Start chat"
             iconPath="img/icons-people.svg"
             size={props.size}

From cafbe1458918d5970a14a933778f4765a519b902 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 8 May 2017 11:59:06 +0100
Subject: [PATCH 58/73] Fix keys for AddressSelector

Was using a property which just did not exist.
---
 src/components/views/elements/AddressSelector.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js
index 6bad15f7d0..5329994037 100644
--- a/src/components/views/elements/AddressSelector.js
+++ b/src/components/views/elements/AddressSelector.js
@@ -1,5 +1,6 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 Vector Creations Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -138,7 +139,7 @@ export default React.createClass({
                         onClick={this.onClick.bind(this, i)}
                         onMouseEnter={this.onMouseEnter.bind(this, i)}
                         onMouseLeave={this.onMouseLeave}
-                        key={this.props.addressList[i].userId}
+                        key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
                         ref={(ref) => { this.addressListElement = ref; }}
                     >
                         <AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />

From 39323647d13532993872ed870e1f68760e73bf73 Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 8 May 2017 14:01:44 +0100
Subject: [PATCH 59/73] Don't show null URL previews

These are URLs that were spidered by the server without error but yielded an empty response from the server. There's nothing to display, so return an empty div.
---
 src/components/views/rooms/LinkPreviewWidget.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js
index ef8fb29cbc..35e6d28b1f 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.js
@@ -100,7 +100,9 @@ module.exports = React.createClass({
 
     render: function() {
         var p = this.state.preview;
-        if (!p) return <div/>;
+        if (!p || Object.keys(p).length === 0) {
+            return <div/>;
+        }
 
         // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
         var image = p["og:image"];

From d1f467ee914423e4ea640e8116cb8625c9ba70c8 Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 8 May 2017 14:53:00 +0100
Subject: [PATCH 60/73] Remove auto-generated component-index

This will now be generated as part of the build process (`npm run build`) and whilst developing (`npm run start`).
---
 src/component-index.js | 265 -----------------------------------------
 1 file changed, 265 deletions(-)
 delete mode 100644 src/component-index.js

diff --git a/src/component-index.js b/src/component-index.js
deleted file mode 100644
index 090a27d5ed..0000000000
--- a/src/component-index.js
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
-Copyright 2015, 2016 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.
-*/
-
-/*
- * THIS FILE IS AUTO-GENERATED
- * You can edit it you like, but your changes will be overwritten,
- * so you'd just be trying to swim upstream like a salmon.
- * You are not a salmon.
- *
- * To update it, run:
- *    ./reskindex.js -h header
- */
-
-module.exports.components = {};
-import structures$ContextualMenu from './components/structures/ContextualMenu';
-structures$ContextualMenu && (module.exports.components['structures.ContextualMenu'] = structures$ContextualMenu);
-import structures$CreateRoom from './components/structures/CreateRoom';
-structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom);
-import structures$FilePanel from './components/structures/FilePanel';
-structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel);
-import structures$InteractiveAuth from './components/structures/InteractiveAuth';
-structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth);
-import structures$LoggedInView from './components/structures/LoggedInView';
-structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView);
-import structures$MatrixChat from './components/structures/MatrixChat';
-structures$MatrixChat && (module.exports.components['structures.MatrixChat'] = structures$MatrixChat);
-import structures$MessagePanel from './components/structures/MessagePanel';
-structures$MessagePanel && (module.exports.components['structures.MessagePanel'] = structures$MessagePanel);
-import structures$NotificationPanel from './components/structures/NotificationPanel';
-structures$NotificationPanel && (module.exports.components['structures.NotificationPanel'] = structures$NotificationPanel);
-import structures$RoomStatusBar from './components/structures/RoomStatusBar';
-structures$RoomStatusBar && (module.exports.components['structures.RoomStatusBar'] = structures$RoomStatusBar);
-import structures$RoomView from './components/structures/RoomView';
-structures$RoomView && (module.exports.components['structures.RoomView'] = structures$RoomView);
-import structures$ScrollPanel from './components/structures/ScrollPanel';
-structures$ScrollPanel && (module.exports.components['structures.ScrollPanel'] = structures$ScrollPanel);
-import structures$TimelinePanel from './components/structures/TimelinePanel';
-structures$TimelinePanel && (module.exports.components['structures.TimelinePanel'] = structures$TimelinePanel);
-import structures$UploadBar from './components/structures/UploadBar';
-structures$UploadBar && (module.exports.components['structures.UploadBar'] = structures$UploadBar);
-import structures$UserSettings from './components/structures/UserSettings';
-structures$UserSettings && (module.exports.components['structures.UserSettings'] = structures$UserSettings);
-import structures$login$ForgotPassword from './components/structures/login/ForgotPassword';
-structures$login$ForgotPassword && (module.exports.components['structures.login.ForgotPassword'] = structures$login$ForgotPassword);
-import structures$login$Login from './components/structures/login/Login';
-structures$login$Login && (module.exports.components['structures.login.Login'] = structures$login$Login);
-import structures$login$PostRegistration from './components/structures/login/PostRegistration';
-structures$login$PostRegistration && (module.exports.components['structures.login.PostRegistration'] = structures$login$PostRegistration);
-import structures$login$Registration from './components/structures/login/Registration';
-structures$login$Registration && (module.exports.components['structures.login.Registration'] = structures$login$Registration);
-import views$avatars$BaseAvatar from './components/views/avatars/BaseAvatar';
-views$avatars$BaseAvatar && (module.exports.components['views.avatars.BaseAvatar'] = views$avatars$BaseAvatar);
-import views$avatars$MemberAvatar from './components/views/avatars/MemberAvatar';
-views$avatars$MemberAvatar && (module.exports.components['views.avatars.MemberAvatar'] = views$avatars$MemberAvatar);
-import views$avatars$RoomAvatar from './components/views/avatars/RoomAvatar';
-views$avatars$RoomAvatar && (module.exports.components['views.avatars.RoomAvatar'] = views$avatars$RoomAvatar);
-import views$create_room$CreateRoomButton from './components/views/create_room/CreateRoomButton';
-views$create_room$CreateRoomButton && (module.exports.components['views.create_room.CreateRoomButton'] = views$create_room$CreateRoomButton);
-import views$create_room$Presets from './components/views/create_room/Presets';
-views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets);
-import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
-views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
-import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
-views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
-import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog';
-views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog);
-import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
-views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
-import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog';
-views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog);
-import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
-views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
-import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
-views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog);
-import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
-views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
-import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';
-views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog);
-import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog';
-views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
-import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
-views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog);
-import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog';
-views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog);
-import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog';
-views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
-import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
-views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
-import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog';
-views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
-import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
-views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
-import views$elements$ActionButton from './components/views/elements/ActionButton';
-views$elements$ActionButton && (module.exports.components['views.elements.ActionButton'] = views$elements$ActionButton);
-import views$elements$AddressSelector from './components/views/elements/AddressSelector';
-views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
-import views$elements$AddressTile from './components/views/elements/AddressTile';
-views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile);
-import views$elements$CreateRoomButton from './components/views/elements/CreateRoomButton';
-views$elements$CreateRoomButton && (module.exports.components['views.elements.CreateRoomButton'] = views$elements$CreateRoomButton);
-import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons';
-views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
-import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
-views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
-import views$elements$Dropdown from './components/views/elements/Dropdown';
-views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown);
-import views$elements$EditableText from './components/views/elements/EditableText';
-views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
-import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
-views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
-import views$elements$EmojiText from './components/views/elements/EmojiText';
-views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText);
-import views$elements$HomeButton from './components/views/elements/HomeButton';
-views$elements$HomeButton && (module.exports.components['views.elements.HomeButton'] = views$elements$HomeButton);
-import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary';
-views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
-import views$elements$PowerSelector from './components/views/elements/PowerSelector';
-views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
-import views$elements$ProgressBar from './components/views/elements/ProgressBar';
-views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar);
-import views$elements$RoomDirectoryButton from './components/views/elements/RoomDirectoryButton';
-views$elements$RoomDirectoryButton && (module.exports.components['views.elements.RoomDirectoryButton'] = views$elements$RoomDirectoryButton);
-import views$elements$SettingsButton from './components/views/elements/SettingsButton';
-views$elements$SettingsButton && (module.exports.components['views.elements.SettingsButton'] = views$elements$SettingsButton);
-import views$elements$StartChatButton from './components/views/elements/StartChatButton';
-views$elements$StartChatButton && (module.exports.components['views.elements.StartChatButton'] = views$elements$StartChatButton);
-import views$elements$TintableSvg from './components/views/elements/TintableSvg';
-views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
-import views$elements$TruncatedList from './components/views/elements/TruncatedList';
-views$elements$TruncatedList && (module.exports.components['views.elements.TruncatedList'] = views$elements$TruncatedList);
-import views$elements$UserSelector from './components/views/elements/UserSelector';
-views$elements$UserSelector && (module.exports.components['views.elements.UserSelector'] = views$elements$UserSelector);
-import views$login$CaptchaForm from './components/views/login/CaptchaForm';
-views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
-import views$login$CasLogin from './components/views/login/CasLogin';
-views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
-import views$login$CountryDropdown from './components/views/login/CountryDropdown';
-views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown);
-import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
-views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
-import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
-views$login$InteractiveAuthEntryComponents && (module.exports.components['views.login.InteractiveAuthEntryComponents'] = views$login$InteractiveAuthEntryComponents);
-import views$login$LoginFooter from './components/views/login/LoginFooter';
-views$login$LoginFooter && (module.exports.components['views.login.LoginFooter'] = views$login$LoginFooter);
-import views$login$LoginHeader from './components/views/login/LoginHeader';
-views$login$LoginHeader && (module.exports.components['views.login.LoginHeader'] = views$login$LoginHeader);
-import views$login$PasswordLogin from './components/views/login/PasswordLogin';
-views$login$PasswordLogin && (module.exports.components['views.login.PasswordLogin'] = views$login$PasswordLogin);
-import views$login$RegistrationForm from './components/views/login/RegistrationForm';
-views$login$RegistrationForm && (module.exports.components['views.login.RegistrationForm'] = views$login$RegistrationForm);
-import views$login$ServerConfig from './components/views/login/ServerConfig';
-views$login$ServerConfig && (module.exports.components['views.login.ServerConfig'] = views$login$ServerConfig);
-import views$messages$MAudioBody from './components/views/messages/MAudioBody';
-views$messages$MAudioBody && (module.exports.components['views.messages.MAudioBody'] = views$messages$MAudioBody);
-import views$messages$MFileBody from './components/views/messages/MFileBody';
-views$messages$MFileBody && (module.exports.components['views.messages.MFileBody'] = views$messages$MFileBody);
-import views$messages$MImageBody from './components/views/messages/MImageBody';
-views$messages$MImageBody && (module.exports.components['views.messages.MImageBody'] = views$messages$MImageBody);
-import views$messages$MVideoBody from './components/views/messages/MVideoBody';
-views$messages$MVideoBody && (module.exports.components['views.messages.MVideoBody'] = views$messages$MVideoBody);
-import views$messages$MessageEvent from './components/views/messages/MessageEvent';
-views$messages$MessageEvent && (module.exports.components['views.messages.MessageEvent'] = views$messages$MessageEvent);
-import views$messages$SenderProfile from './components/views/messages/SenderProfile';
-views$messages$SenderProfile && (module.exports.components['views.messages.SenderProfile'] = views$messages$SenderProfile);
-import views$messages$TextualBody from './components/views/messages/TextualBody';
-views$messages$TextualBody && (module.exports.components['views.messages.TextualBody'] = views$messages$TextualBody);
-import views$messages$TextualEvent from './components/views/messages/TextualEvent';
-views$messages$TextualEvent && (module.exports.components['views.messages.TextualEvent'] = views$messages$TextualEvent);
-import views$messages$UnknownBody from './components/views/messages/UnknownBody';
-views$messages$UnknownBody && (module.exports.components['views.messages.UnknownBody'] = views$messages$UnknownBody);
-import views$room_settings$AliasSettings from './components/views/room_settings/AliasSettings';
-views$room_settings$AliasSettings && (module.exports.components['views.room_settings.AliasSettings'] = views$room_settings$AliasSettings);
-import views$room_settings$ColorSettings from './components/views/room_settings/ColorSettings';
-views$room_settings$ColorSettings && (module.exports.components['views.room_settings.ColorSettings'] = views$room_settings$ColorSettings);
-import views$room_settings$UrlPreviewSettings from './components/views/room_settings/UrlPreviewSettings';
-views$room_settings$UrlPreviewSettings && (module.exports.components['views.room_settings.UrlPreviewSettings'] = views$room_settings$UrlPreviewSettings);
-import views$rooms$Autocomplete from './components/views/rooms/Autocomplete';
-views$rooms$Autocomplete && (module.exports.components['views.rooms.Autocomplete'] = views$rooms$Autocomplete);
-import views$rooms$AuxPanel from './components/views/rooms/AuxPanel';
-views$rooms$AuxPanel && (module.exports.components['views.rooms.AuxPanel'] = views$rooms$AuxPanel);
-import views$rooms$EntityTile from './components/views/rooms/EntityTile';
-views$rooms$EntityTile && (module.exports.components['views.rooms.EntityTile'] = views$rooms$EntityTile);
-import views$rooms$EventTile from './components/views/rooms/EventTile';
-views$rooms$EventTile && (module.exports.components['views.rooms.EventTile'] = views$rooms$EventTile);
-import views$rooms$LinkPreviewWidget from './components/views/rooms/LinkPreviewWidget';
-views$rooms$LinkPreviewWidget && (module.exports.components['views.rooms.LinkPreviewWidget'] = views$rooms$LinkPreviewWidget);
-import views$rooms$MemberDeviceInfo from './components/views/rooms/MemberDeviceInfo';
-views$rooms$MemberDeviceInfo && (module.exports.components['views.rooms.MemberDeviceInfo'] = views$rooms$MemberDeviceInfo);
-import views$rooms$MemberInfo from './components/views/rooms/MemberInfo';
-views$rooms$MemberInfo && (module.exports.components['views.rooms.MemberInfo'] = views$rooms$MemberInfo);
-import views$rooms$MemberList from './components/views/rooms/MemberList';
-views$rooms$MemberList && (module.exports.components['views.rooms.MemberList'] = views$rooms$MemberList);
-import views$rooms$MemberTile from './components/views/rooms/MemberTile';
-views$rooms$MemberTile && (module.exports.components['views.rooms.MemberTile'] = views$rooms$MemberTile);
-import views$rooms$MessageComposer from './components/views/rooms/MessageComposer';
-views$rooms$MessageComposer && (module.exports.components['views.rooms.MessageComposer'] = views$rooms$MessageComposer);
-import views$rooms$MessageComposerInput from './components/views/rooms/MessageComposerInput';
-views$rooms$MessageComposerInput && (module.exports.components['views.rooms.MessageComposerInput'] = views$rooms$MessageComposerInput);
-import views$rooms$MessageComposerInputOld from './components/views/rooms/MessageComposerInputOld';
-views$rooms$MessageComposerInputOld && (module.exports.components['views.rooms.MessageComposerInputOld'] = views$rooms$MessageComposerInputOld);
-import views$rooms$PresenceLabel from './components/views/rooms/PresenceLabel';
-views$rooms$PresenceLabel && (module.exports.components['views.rooms.PresenceLabel'] = views$rooms$PresenceLabel);
-import views$rooms$ReadReceiptMarker from './components/views/rooms/ReadReceiptMarker';
-views$rooms$ReadReceiptMarker && (module.exports.components['views.rooms.ReadReceiptMarker'] = views$rooms$ReadReceiptMarker);
-import views$rooms$RoomHeader from './components/views/rooms/RoomHeader';
-views$rooms$RoomHeader && (module.exports.components['views.rooms.RoomHeader'] = views$rooms$RoomHeader);
-import views$rooms$RoomList from './components/views/rooms/RoomList';
-views$rooms$RoomList && (module.exports.components['views.rooms.RoomList'] = views$rooms$RoomList);
-import views$rooms$RoomNameEditor from './components/views/rooms/RoomNameEditor';
-views$rooms$RoomNameEditor && (module.exports.components['views.rooms.RoomNameEditor'] = views$rooms$RoomNameEditor);
-import views$rooms$RoomPreviewBar from './components/views/rooms/RoomPreviewBar';
-views$rooms$RoomPreviewBar && (module.exports.components['views.rooms.RoomPreviewBar'] = views$rooms$RoomPreviewBar);
-import views$rooms$RoomSettings from './components/views/rooms/RoomSettings';
-views$rooms$RoomSettings && (module.exports.components['views.rooms.RoomSettings'] = views$rooms$RoomSettings);
-import views$rooms$RoomTile from './components/views/rooms/RoomTile';
-views$rooms$RoomTile && (module.exports.components['views.rooms.RoomTile'] = views$rooms$RoomTile);
-import views$rooms$RoomTopicEditor from './components/views/rooms/RoomTopicEditor';
-views$rooms$RoomTopicEditor && (module.exports.components['views.rooms.RoomTopicEditor'] = views$rooms$RoomTopicEditor);
-import views$rooms$SearchResultTile from './components/views/rooms/SearchResultTile';
-views$rooms$SearchResultTile && (module.exports.components['views.rooms.SearchResultTile'] = views$rooms$SearchResultTile);
-import views$rooms$SearchableEntityList from './components/views/rooms/SearchableEntityList';
-views$rooms$SearchableEntityList && (module.exports.components['views.rooms.SearchableEntityList'] = views$rooms$SearchableEntityList);
-import views$rooms$SimpleRoomHeader from './components/views/rooms/SimpleRoomHeader';
-views$rooms$SimpleRoomHeader && (module.exports.components['views.rooms.SimpleRoomHeader'] = views$rooms$SimpleRoomHeader);
-import views$rooms$TabCompleteBar from './components/views/rooms/TabCompleteBar';
-views$rooms$TabCompleteBar && (module.exports.components['views.rooms.TabCompleteBar'] = views$rooms$TabCompleteBar);
-import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnreadMessagesBar';
-views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
-import views$rooms$UserTile from './components/views/rooms/UserTile';
-views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile);
-import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber';
-views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber);
-import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
-views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
-import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
-views$settings$ChangeDisplayName && (module.exports.components['views.settings.ChangeDisplayName'] = views$settings$ChangeDisplayName);
-import views$settings$ChangePassword from './components/views/settings/ChangePassword';
-views$settings$ChangePassword && (module.exports.components['views.settings.ChangePassword'] = views$settings$ChangePassword);
-import views$settings$DevicesPanel from './components/views/settings/DevicesPanel';
-views$settings$DevicesPanel && (module.exports.components['views.settings.DevicesPanel'] = views$settings$DevicesPanel);
-import views$settings$DevicesPanelEntry from './components/views/settings/DevicesPanelEntry';
-views$settings$DevicesPanelEntry && (module.exports.components['views.settings.DevicesPanelEntry'] = views$settings$DevicesPanelEntry);
-import views$settings$EnableNotificationsButton from './components/views/settings/EnableNotificationsButton';
-views$settings$EnableNotificationsButton && (module.exports.components['views.settings.EnableNotificationsButton'] = views$settings$EnableNotificationsButton);
-import views$voip$CallView from './components/views/voip/CallView';
-views$voip$CallView && (module.exports.components['views.voip.CallView'] = views$voip$CallView);
-import views$voip$IncomingCallBox from './components/views/voip/IncomingCallBox';
-views$voip$IncomingCallBox && (module.exports.components['views.voip.IncomingCallBox'] = views$voip$IncomingCallBox);
-import views$voip$VideoFeed from './components/views/voip/VideoFeed';
-views$voip$VideoFeed && (module.exports.components['views.voip.VideoFeed'] = views$voip$VideoFeed);
-import views$voip$VideoView from './components/views/voip/VideoView';
-views$voip$VideoView && (module.exports.components['views.voip.VideoView'] = views$voip$VideoView);

From ca7989a1fdd0d9b69f29bcad06ebf7580d68bd65 Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 8 May 2017 14:55:55 +0100
Subject: [PATCH 61/73] .gitignore auto-generated component-index

---
 .gitignore | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.gitignore b/.gitignore
index 5139d614ad..dcfe1c355d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@ npm-debug.log
 
 # test reports created by karma
 /karma-reports
+
+# ignore auto-generated component index
+/src/component-index.js

From 1addc7e2c573394696a0a1e1cda675cb3a2c0b62 Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 8 May 2017 14:56:38 +0100
Subject: [PATCH 62/73] Update header copyright

---
 header | 1 +
 1 file changed, 1 insertion(+)

diff --git a/header b/header
index 060709b82e..beee1ebe89 100644
--- a/header
+++ b/header
@@ -1,5 +1,6 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 Vector Creations Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From 2eaaa974516ce224602b0295f937549de1bb1b8e Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 8 May 2017 14:57:04 +0100
Subject: [PATCH 63/73] Give `reskindex.js` a watch mode (-w)

`scripts/reskindex.js -w` will run reskindex in watch mode whereby FS events will cause a reskindex to occur.

This depends on `chokidar`
---
 package.json         |  1 +
 scripts/reskindex.js | 92 +++++++++++++++++++++++++-------------------
 2 files changed, 54 insertions(+), 39 deletions(-)

diff --git a/package.json b/package.json
index 2001f0d4ad..b019c63da5 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
     "blueimp-canvas-to-blob": "^3.5.0",
     "browser-encrypt-attachment": "^0.3.0",
     "browser-request": "^0.3.3",
+    "chokidar": "^1.6.1",
     "classnames": "^2.1.2",
     "commonmark": "^0.27.0",
     "draft-js": "^0.8.1",
diff --git a/scripts/reskindex.js b/scripts/reskindex.js
index f9cbc2a711..e82104f35c 100755
--- a/scripts/reskindex.js
+++ b/scripts/reskindex.js
@@ -1,53 +1,67 @@
 #!/usr/bin/env node
-
 var fs = require('fs');
 var path = require('path');
 var glob = require('glob');
-
 var args = require('optimist').argv;
-
-var header = args.h || args.header;
-
-var componentsDir = path.join('src', 'components');
+var chokidar = require('chokidar');
 
 var componentIndex = path.join('src', 'component-index.js');
+var componentsDir = path.join('src', 'components');
 
-var packageJson = JSON.parse(fs.readFileSync('./package.json'));
+function reskindex() {
+    var header = args.h || args.header;
+    var packageJson = JSON.parse(fs.readFileSync('./package.json'));
 
-var strm = fs.createWriteStream(componentIndex);
+    var strm = fs.createWriteStream(componentIndex);
 
-if (header) {
-   strm.write(fs.readFileSync(header));
-   strm.write('\n');
+    if (header) {
+       strm.write(fs.readFileSync(header));
+       strm.write('\n');
+    }
+
+    strm.write("/*\n");
+    strm.write(" * THIS FILE IS AUTO-GENERATED\n");
+    strm.write(" * You can edit it you like, but your changes will be overwritten,\n");
+    strm.write(" * so you'd just be trying to swim upstream like a salmon.\n");
+    strm.write(" * You are not a salmon.\n");
+    strm.write(" */\n\n");
+
+    if (packageJson['matrix-react-parent']) {
+        strm.write(
+            "module.exports.components = require('"+
+            packageJson['matrix-react-parent']+
+            "/lib/component-index').components;\n\n"
+        );
+    } else {
+        strm.write("module.exports.components = {};\n");
+    }
+
+    var files = glob.sync('**/*.js', {cwd: componentsDir}).sort();
+    for (var i = 0; i < files.length; ++i) {
+        var file = files[i].replace('.js', '');
+
+        var moduleName = (file.replace(/\//g, '.'));
+        var importName = moduleName.replace(/\./g, "$");
+
+        strm.write("import " + importName + " from './components/" + file + "';\n");
+        strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");");
+        strm.write('\n');
+        strm.uncork();
+    }
+
+    strm.end();
+    console.log('Reskindex: completed');
 }
 
-strm.write("/*\n");
-strm.write(" * THIS FILE IS AUTO-GENERATED\n");
-strm.write(" * You can edit it you like, but your changes will be overwritten,\n");
-strm.write(" * so you'd just be trying to swim upstream like a salmon.\n");
-strm.write(" * You are not a salmon.\n");
-strm.write(" *\n");
-strm.write(" * To update it, run:\n");
-strm.write(" *    ./reskindex.js -h header\n");
-strm.write(" */\n\n");
-
-if (packageJson['matrix-react-parent']) {
-    strm.write("module.exports.components = require('"+packageJson['matrix-react-parent']+"/lib/component-index').components;\n\n");
-} else {
-    strm.write("module.exports.components = {};\n");
+// -w indicates watch mode where any FS events will trigger reskindex
+if (!args.w) {
+    reskindex();
+    return;
 }
 
-var files = glob.sync('**/*.js', {cwd: componentsDir}).sort();
-for (var i = 0; i < files.length; ++i) {
-    var file = files[i].replace('.js', '');
-
-    var moduleName = (file.replace(/\//g, '.'));
-    var importName = moduleName.replace(/\./g, "$");
-
-    strm.write("import " + importName + " from './components/" + file + "';\n");
-    strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");");
-    strm.write('\n');
-    strm.uncork();
-}
-
-strm.end();
+var watchDebouncer = null;
+chokidar.watch('./src').on('all', (event, path) => {
+    if (path === componentIndex) return;
+    if (watchDebouncer) clearTimeout(watchDebouncer);
+    watchDebouncer = setTimeout(reskindex, 1000);
+});

From 7d1940620db557203887b710292c23cfff2a910e Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 8 May 2017 15:07:58 +0100
Subject: [PATCH 64/73] Add (watching) reskindex to `npm start`

also add reskindex in non-watching mode to `npm run build`
---
 package.json | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index b019c63da5..28f77f9460 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,10 @@
   },
   "scripts": {
     "reskindex": "scripts/reskindex.js -h header",
-    "build": "babel src -d lib --source-maps",
-    "start": "babel src -w -d lib --source-maps",
+    "reskindex:watch": "scripts/reskindex.js -h header -w",
+    "build": "npm run reskindex && babel src -d lib --source-maps",
+    "build:watch": "babel src -w -d lib --source-maps",
+    "start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
     "lint": "eslint src/",
     "lintall": "eslint src/ test/",
     "clean": "rimraf lib",
@@ -106,6 +108,7 @@
     "karma-sourcemap-loader": "^0.3.7",
     "karma-webpack": "^1.7.0",
     "mocha": "^2.4.5",
+    "parallelshell": "^1.2.0",
     "phantomjs-prebuilt": "^2.1.7",
     "react-addons-test-utils": "^15.4.0",
     "require-json": "0.0.1",

From 534f9277d436e66725a7f8875d88d78f7cf59f02 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 8 May 2017 15:37:40 +0100
Subject: [PATCH 65/73] Fix this/self fail in LeftPanel

---
 src/components/views/dialogs/ChatInviteDialog.js |  5 +++++
 src/components/views/rooms/RoomList.js           | 14 +++++++-------
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js
index 7ba503099a..b349b94e5d 100644
--- a/src/components/views/dialogs/ChatInviteDialog.js
+++ b/src/components/views/dialogs/ChatInviteDialog.js
@@ -191,6 +191,7 @@ module.exports = React.createClass({
         this.queryChangedDebouncer = setTimeout(() => {
             // Only do search if there is something to search
             if (query.length > 0 && query != '@') {
+                performance.mark('start');
                 // Weighted keys prefer to match userIds when first char is @
                 this._fuse.options.keys = [{
                     name: 'displayName',
@@ -199,6 +200,7 @@ module.exports = React.createClass({
                     name: 'userId',
                     weight: query[0] === '@' ? 0.9 : 0.1,
                 }];
+                performance.mark('middle');
                 queryList = this._fuse.search(query).map((user) => {
                     // Return objects, structure of which is defined
                     // by InviteAddressType
@@ -210,6 +212,9 @@ module.exports = React.createClass({
                         isKnown: true,
                     }
                 });
+                performance.mark('end');
+                performance.measure('setopts', 'start', 'middle');
+                performance.measure('search', 'middle', 'end');
 
                 // If the query is a valid address, add an entry for that
                 // This is important, otherwise there's no way to invite
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 8d396b5536..a595a91ba9 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -652,7 +652,7 @@ module.exports = React.createClass({
                 <RoomSubList list={ self.state.lists['m.favourite'] }
                              label="Favourites"
                              tagName="m.favourite"
-                             emptyContent={this._getEmptyContent('m.favourite')}
+                             emptyContent={self._getEmptyContent('m.favourite')}
                              editable={ true }
                              order="manual"
                              incomingCall={ self.state.incomingCall }
@@ -665,8 +665,8 @@ module.exports = React.createClass({
                 <RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
                              label="People"
                              tagName="im.vector.fake.direct"
-                             emptyContent={this._getEmptyContent('im.vector.fake.direct')}
-                             headerItems={this._getHeaderItems('im.vector.fake.direct')}
+                             emptyContent={self._getEmptyContent('im.vector.fake.direct')}
+                             headerItems={self._getHeaderItems('im.vector.fake.direct')}
                              editable={ true }
                              order="recent"
                              incomingCall={ self.state.incomingCall }
@@ -681,8 +681,8 @@ module.exports = React.createClass({
                              label="Rooms"
                              tagName="im.vector.fake.recent"
                              editable={ true }
-                             emptyContent={this._getEmptyContent('im.vector.fake.recent')}
-                             headerItems={this._getHeaderItems('im.vector.fake.recent')}
+                             emptyContent={self._getEmptyContent('im.vector.fake.recent')}
+                             headerItems={self._getHeaderItems('im.vector.fake.recent')}
                              order="recent"
                              incomingCall={ self.state.incomingCall }
                              collapsed={ self.props.collapsed }
@@ -697,7 +697,7 @@ module.exports = React.createClass({
                              key={ tagName }
                              label={ tagName }
                              tagName={ tagName }
-                             emptyContent={this._getEmptyContent(tagName)}
+                             emptyContent={self._getEmptyContent(tagName)}
                              editable={ true }
                              order="manual"
                              incomingCall={ self.state.incomingCall }
@@ -713,7 +713,7 @@ module.exports = React.createClass({
                 <RoomSubList list={ self.state.lists['m.lowpriority'] }
                              label="Low priority"
                              tagName="m.lowpriority"
-                             emptyContent={this._getEmptyContent('m.lowpriority')}
+                             emptyContent={self._getEmptyContent('m.lowpriority')}
                              editable={ true }
                              order="recent"
                              incomingCall={ self.state.incomingCall }

From 9cae667c063e67b32e60b89e7256d714a056559b Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 8 May 2017 16:03:52 +0100
Subject: [PATCH 66/73] Fix Create Room button

Opened the DM dialog rather than the new room dialog
---
 src/components/views/elements/CreateRoomButton.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js
index e7e526d36b..73c984a860 100644
--- a/src/components/views/elements/CreateRoomButton.js
+++ b/src/components/views/elements/CreateRoomButton.js
@@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
 const CreateRoomButton = function(props) {
     const ActionButton = sdk.getComponent('elements.ActionButton');
     return (
-        <ActionButton action="view_create_chat"
+        <ActionButton action="view_create_room"
             label="Create new room"
             iconPath="img/icons-create-room.svg"
             size={props.size}

From 574c836941ca05164817a5458b5b7bf14b918a1a Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 8 May 2017 16:24:13 +0100
Subject: [PATCH 67/73] Move chokidar to devDeps

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 28f77f9460..4b0e8de5cd 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,6 @@
     "blueimp-canvas-to-blob": "^3.5.0",
     "browser-encrypt-attachment": "^0.3.0",
     "browser-request": "^0.3.3",
-    "chokidar": "^1.6.1",
     "classnames": "^2.1.2",
     "commonmark": "^0.27.0",
     "draft-js": "^0.8.1",
@@ -92,6 +91,7 @@
     "babel-preset-es2016": "^6.11.3",
     "babel-preset-es2017": "^6.14.0",
     "babel-preset-react": "^6.11.1",
+    "chokidar": "^1.6.1",
     "eslint": "^3.13.1",
     "eslint-config-google": "^0.7.1",
     "eslint-plugin-babel": "^4.0.1",

From 9af9603373ea9a3459170fc02413ae74200a04da Mon Sep 17 00:00:00 2001
From: Luke Barnard <lukeb@openmarket.com>
Date: Mon, 8 May 2017 16:29:53 +0100
Subject: [PATCH 68/73] Only watch indexed files

---
 scripts/reskindex.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/scripts/reskindex.js b/scripts/reskindex.js
index e82104f35c..1db22f9e10 100755
--- a/scripts/reskindex.js
+++ b/scripts/reskindex.js
@@ -7,6 +7,7 @@ var chokidar = require('chokidar');
 
 var componentIndex = path.join('src', 'component-index.js');
 var componentsDir = path.join('src', 'components');
+var componentGlob = '**/*.js';
 
 function reskindex() {
     var header = args.h || args.header;
@@ -36,7 +37,7 @@ function reskindex() {
         strm.write("module.exports.components = {};\n");
     }
 
-    var files = glob.sync('**/*.js', {cwd: componentsDir}).sort();
+    var files = glob.sync(componentGlob, {cwd: componentsDir}).sort();
     for (var i = 0; i < files.length; ++i) {
         var file = files[i].replace('.js', '');
 
@@ -60,7 +61,7 @@ if (!args.w) {
 }
 
 var watchDebouncer = null;
-chokidar.watch('./src').on('all', (event, path) => {
+chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => {
     if (path === componentIndex) return;
     if (watchDebouncer) clearTimeout(watchDebouncer);
     watchDebouncer = setTimeout(reskindex, 1000);

From 805354bd2c3d2e6233c55833f561d489d8b40445 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 8 May 2017 16:39:11 +0100
Subject: [PATCH 69/73] Revert unintentional change

---
 src/components/views/dialogs/ChatInviteDialog.js | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js
index b349b94e5d..7ba503099a 100644
--- a/src/components/views/dialogs/ChatInviteDialog.js
+++ b/src/components/views/dialogs/ChatInviteDialog.js
@@ -191,7 +191,6 @@ module.exports = React.createClass({
         this.queryChangedDebouncer = setTimeout(() => {
             // Only do search if there is something to search
             if (query.length > 0 && query != '@') {
-                performance.mark('start');
                 // Weighted keys prefer to match userIds when first char is @
                 this._fuse.options.keys = [{
                     name: 'displayName',
@@ -200,7 +199,6 @@ module.exports = React.createClass({
                     name: 'userId',
                     weight: query[0] === '@' ? 0.9 : 0.1,
                 }];
-                performance.mark('middle');
                 queryList = this._fuse.search(query).map((user) => {
                     // Return objects, structure of which is defined
                     // by InviteAddressType
@@ -212,9 +210,6 @@ module.exports = React.createClass({
                         isKnown: true,
                     }
                 });
-                performance.mark('end');
-                performance.measure('setopts', 'start', 'middle');
-                performance.measure('search', 'middle', 'end');
 
                 // If the query is a valid address, add an entry for that
                 // This is important, otherwise there's no way to invite

From 85ed39b9d87214a069af6c6ab2b642b6ff8b3386 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 8 May 2017 16:49:40 +0100
Subject: [PATCH 70/73] Put room name in 'leave room' confirmation dialog

https://github.com/vector-im/riot-web/issues/3850
---
 src/components/structures/MatrixChat.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 9b8aa3426a..8865d77d51 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -392,9 +392,10 @@ module.exports = React.createClass({
                 this.notifyNewScreen('forgot_password');
                 break;
             case 'leave_room':
+                const roomToLeave = MatrixClientPeg.get().getRoom(payload.room_id);
                 Modal.createDialog(QuestionDialog, {
                     title: "Leave room",
-                    description: "Are you sure you want to leave the room?",
+                    description: <span>Are you sure you want to leave the room <i>{roomToLeave.name}</i>?</span>,
                     onFinished: (should_leave) => {
                         if (should_leave) {
                             const d = MatrixClientPeg.get().leave(payload.room_id);

From f02e659fb7502ebb524651494e9c9bd1098fcc9b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Tue, 9 May 2017 11:27:06 +0100
Subject: [PATCH 71/73] Consume key{up,down,pressed} events

so they don't trigger other things bubbling up
until Modal is closed
---
 src/components/views/dialogs/BaseDialog.js | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index ac36dfd056..02460148b3 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -57,28 +57,25 @@ export default React.createClass({
         }
     },
 
-    // Don't let esc keydown events get any further, so they only trigger this and nothing more
-    _onKeyDown: function(e) {
-        if (e.keyCode === KeyCode.ESCAPE) {
-            e.stopPropagation();
-            e.preventDefault();
-        }
+    // Don't let key{down,press} events escape the modal. Consume them all.
+    _eatKeyEvent: function(e) {
+        e.stopPropagation();
     },
     
     // Must be when the key is released (and not pressed) otherwise componentWillUnmount
     // will focus another element which will receive future key events
     _onKeyUp: function(e) {
         if (e.keyCode === KeyCode.ESCAPE) {
-            e.stopPropagation();
             e.preventDefault();
             this.props.onFinished();
         } else if (e.keyCode === KeyCode.ENTER) {
             if (this.props.onEnterPressed) {
-                e.stopPropagation();
                 e.preventDefault();
                 this.props.onEnterPressed(e);
             }
         }
+        // Consume all keyup events while Modal is open
+        e.stopPropagation();
     },
 
     _onCancelClick: function(e) {
@@ -89,7 +86,11 @@ export default React.createClass({
         const TintableSvg = sdk.getComponent("elements.TintableSvg");
 
         return (
-            <div onKeyDown={this._onKeyDown} onKeyUp={this._onKeyUp} className={this.props.className}>
+            <div onKeyUp={this._onKeyUp}
+                 onKeyDown={this._eatKeyEvent}
+                 onKeyPress={this._eatKeyEvent}
+                 className={this.props.className}
+            >
                 <AccessibleButton onClick={this._onCancelClick}
                     className="mx_Dialog_cancelButton"
                 >

From fdf48def00d336dd55ee189d21ea854cc72fd1d8 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@googlemail.com>
Date: Tue, 9 May 2017 17:13:27 +0100
Subject: [PATCH 72/73] make reskindex windows friendly

makes #871 windows friendly
---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 4b0e8de5cd..21add8ccb7 100644
--- a/package.json
+++ b/package.json
@@ -31,8 +31,8 @@
     "reskindex": "scripts/reskindex.js"
   },
   "scripts": {
-    "reskindex": "scripts/reskindex.js -h header",
-    "reskindex:watch": "scripts/reskindex.js -h header -w",
+    "reskindex": "node scripts/reskindex.js -h header",
+    "reskindex:watch": "node scripts/reskindex.js -h header -w",
     "build": "npm run reskindex && babel src -d lib --source-maps",
     "build:watch": "babel src -w -d lib --source-maps",
     "start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",

From 50092a0f1f432ad8b7aed33120765b264e32e7cb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 10 May 2017 15:16:49 +0100
Subject: [PATCH 73/73] fixes vector-im/riot-web#3881

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/rooms/RoomSettings.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index 2c29dd433c..798aacfa8e 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -926,7 +926,7 @@ module.exports = React.createClass({
                         <PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
                     </div>
                     <div className="mx_RoomSettings_powerLevel">
-                        <span className="mx_RoomSettings_powerLevelKey">To redact messages, you must be a </span>
+                        <span className="mx_RoomSettings_powerLevelKey">To redact other users' messages, you must be a </span>
                         <PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
                     </div>