From 8cf2607415ad7bdb837fa0de15dc9c28a353bae8 Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Tue, 12 Mar 2019 16:30:06 +0100
Subject: [PATCH 01/10] use AutoHideScrollbar in ScrollPanel

---
 res/css/structures/_RoomView.scss             |  2 +
 .../structures/AutoHideScrollbar.js           |  1 +
 src/components/structures/ScrollPanel.js      | 40 +++++++++----------
 3 files changed, 23 insertions(+), 20 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index f15552e484..1b639928e0 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -84,6 +84,7 @@ limitations under the License.
     display: flex;
     flex-direction: column;
     flex: 1;
+    min-width: 0;
 }
 
 .mx_RoomView_body .mx_RoomView_timeline {
@@ -111,6 +112,7 @@ limitations under the License.
 .mx_RoomView_messagePanel {
     width: 100%;
     overflow-y: auto;
+    flex: 1 1 0;
 }
 
 .mx_RoomView_messagePanelSearchSpinner {
diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js
index 0f93f20407..72d48a2084 100644
--- a/src/components/structures/AutoHideScrollbar.js
+++ b/src/components/structures/AutoHideScrollbar.js
@@ -121,6 +121,7 @@ export default class AutoHideScrollbar extends React.Component {
     render() {
         return (<div
                     ref={this._collectContainerRef}
+                    style={this.props.style}
                     className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
                     onScroll={this.props.onScroll}
                 >
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index ee4045c91e..cdb79686ad 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -15,14 +15,13 @@ limitations under the License.
 */
 
 const React = require("react");
-const ReactDOM = require("react-dom");
 import PropTypes from 'prop-types';
 import Promise from 'bluebird';
 import { KeyCode } from '../../Keyboard';
-import sdk from '../../index.js';
+import AutoHideScrollbar from "./AutoHideScrollbar";
 
 const DEBUG_SCROLL = false;
-// var DEBUG_SCROLL = true;
+// const DEBUG_SCROLL = true;
 
 // The amount of extra scroll distance to allow prior to unfilling.
 // See _getExcessHeight.
@@ -129,11 +128,6 @@ module.exports = React.createClass({
          */
         onScroll: PropTypes.func,
 
-        /* onResize: a callback which is called whenever the Gemini scroll
-         * panel is resized
-         */
-        onResize: PropTypes.func,
-
         /* className: classnames to add to the top-level div
          */
         className: PropTypes.string,
@@ -150,7 +144,6 @@ module.exports = React.createClass({
             onFillRequest: function(backwards) { return Promise.resolve(false); },
             onUnfillRequest: function(backwards, scrollToken) {},
             onScroll: function() {},
-            onResize: function() {},
         };
     },
 
@@ -185,6 +178,16 @@ module.exports = React.createClass({
         debuglog("Scroll event: offset now:", sn.scrollTop,
                  "_lastSetScroll:", this._lastSetScroll);
 
+        // ignore scroll events where scrollTop hasn't changed,
+        // appears to happen when the layout changes outside
+        // of the scroll container, like resizing the right panel.
+        if (sn.scrollTop === this._lastEventScroll) {
+            debuglog("ignore scroll event with same scrollTop as before");
+            return;
+        }
+
+        this._lastEventScroll = sn.scrollTop;
+
         // Sometimes we see attempts to write to scrollTop essentially being
         // ignored. (Or rather, it is successfully written, but on the next
         // scroll event, it's been reset again).
@@ -225,9 +228,7 @@ module.exports = React.createClass({
 
     onResize: function() {
         this.clearBlockShrinking();
-        this.props.onResize();
         this.checkScroll();
-        if (this._gemScroll) this._gemScroll.forceUpdate();
     },
 
     // after an update to the contents of the panel, check that the scroll is
@@ -672,17 +673,17 @@ module.exports = React.createClass({
             throw new Error("ScrollPanel._getScrollNode called when unmounted");
         }
 
-        if (!this._gemScroll) {
+        if (!this._divScroll) {
             // Likewise, we should have the ref by this point, but if not
             // turn the NPE into something meaningful.
             throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
         }
 
-        return this._gemScroll.scrollbar.getViewElement();
+        return this._divScroll;
     },
 
-    _collectGeminiScroll: function(gemScroll) {
-        this._gemScroll = gemScroll;
+    _collectScroll: function(divScroll) {
+        this._divScroll = divScroll;
     },
 
     /**
@@ -724,19 +725,18 @@ module.exports = React.createClass({
     },
 
     render: function() {
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
         // TODO: the classnames on the div and ol could do with being updated to
         // reflect the fact that we don't necessarily contain a list of messages.
         // it's not obvious why we have a separate div and ol anyway.
-        return (<GeminiScrollbarWrapper autoshow={true} wrappedRef={this._collectGeminiScroll}
-                onScroll={this.onScroll} onResize={this.onResize}
+        return (<AutoHideScrollbar wrappedRef={this._collectScroll}
+                onScroll={this.onScroll}
                 className={this.props.className} style={this.props.style}>
                     <div className="mx_RoomView_messageListWrapper">
                         <ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
                             { this.props.children }
                         </ol>
                     </div>
-                </GeminiScrollbarWrapper>
-               );
+                </AutoHideScrollbar>
+            );
     },
 });

From 27070b314960ed3cfc0e4415e96534accff821a3 Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Tue, 12 Mar 2019 16:33:05 +0100
Subject: [PATCH 02/10] remove onChildResize in RoomView as it's unused

---
 src/components/structures/RoomView.js             | 15 ++-------------
 src/components/views/rooms/MessageComposer.js     |  5 -----
 .../views/rooms/MessageComposerInput.js           |  4 ----
 3 files changed, 2 insertions(+), 22 deletions(-)

diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 2a1c7fe79e..d7488a2558 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -1355,8 +1355,7 @@ module.exports = React.createClass({
 
         const showBar = this.refs.messagePanel.canJumpToReadMarker();
         if (this.state.showTopUnreadMessagesBar != showBar) {
-            this.setState({showTopUnreadMessagesBar: showBar},
-                          this.onChildResize);
+            this.setState({showTopUnreadMessagesBar: showBar});
         }
     },
 
@@ -1399,7 +1398,7 @@ module.exports = React.createClass({
         };
     },
 
-    onResize: function(e) {
+    onResize: function() {
         // It seems flexbox doesn't give us a way to constrain the auxPanel height to have
         // a minimum of the height of the video element, whilst also capping it from pushing out the page
         // so we have to do it via JS instead.  In this implementation we cap the height by putting
@@ -1417,9 +1416,6 @@ module.exports = React.createClass({
         if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
 
         this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
-
-        // changing the maxHeight on the auxpanel will trigger a callback go
-        // onChildResize, so no need to worry about that here.
     },
 
     onFullscreenClick: function() {
@@ -1449,10 +1445,6 @@ module.exports = React.createClass({
         this.forceUpdate(); // TODO: just update the voip buttons
     },
 
-    onChildResize: function() {
-        // no longer anything to do here
-    },
-
     onStatusBarVisible: function() {
         if (this.unmounted) return;
         this.setState({
@@ -1645,7 +1637,6 @@ module.exports = React.createClass({
                 isPeeking={myMembership !== "join"}
                 onInviteClick={this.onInviteButtonClick}
                 onStopWarningClick={this.onStopAloneWarningClick}
-                onResize={this.onChildResize}
                 onVisible={this.onStatusBarVisible}
                 onHidden={this.onStatusBarHidden}
             />;
@@ -1714,7 +1705,6 @@ module.exports = React.createClass({
               draggingFile={this.state.draggingFile}
               displayConfCallNotification={this.state.displayConfCallNotification}
               maxHeight={this.state.auxPanelMaxHeight}
-              onResize={this.onChildResize}
               showApps={this.state.showApps}
               hideAppsDrawer={false} >
                 { aux }
@@ -1730,7 +1720,6 @@ module.exports = React.createClass({
             messageComposer =
                 <MessageComposer
                     room={this.state.room}
-                    onResize={this.onChildResize}
                     uploadFile={this.uploadFile}
                     callState={this.state.callState}
                     disabled={this.props.disabled}
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 53668066ee..a76a45497b 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -412,7 +412,6 @@ export default class MessageComposer extends React.Component {
                 <MessageComposerInput
                     ref={(c) => this.messageComposerInput = c}
                     key="controls_input"
-                    onResize={this.props.onResize}
                     room={this.props.room}
                     placeholder={placeholderText}
                     onFilesPasted={this.uploadFiles}
@@ -505,10 +504,6 @@ export default class MessageComposer extends React.Component {
 }
 
 MessageComposer.propTypes = {
-    // a callback which is called when the height of the composer is
-    // changed due to a change in content.
-    onResize: PropTypes.func,
-
     // js-sdk Room object
     room: PropTypes.object.isRequired,
 
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 6b80902c8f..cbea2bccb9 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -135,10 +135,6 @@ function rangeEquals(a: Range, b: Range): boolean {
  */
 export default class MessageComposerInput extends React.Component {
     static propTypes = {
-        // a callback which is called when the height of the composer is
-        // changed due to a change in content.
-        onResize: PropTypes.func,
-
         // js-sdk Room object
         room: PropTypes.object.isRequired,
 

From 735b4f6fcfbff30e0064568282f17e4dc8b765bc Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Tue, 12 Mar 2019 16:36:12 +0100
Subject: [PATCH 03/10] create ResizeNotifier to derive which areas of the app
 resize and emit

---
 src/components/structures/FilePanel.js     |  1 +
 src/components/structures/LoggedInView.js  |  2 +
 src/components/structures/MainSplit.js     |  1 +
 src/components/structures/MatrixChat.js    |  3 ++
 src/components/structures/MessagePanel.js  |  3 +-
 src/components/structures/RightPanel.js    |  2 +-
 src/components/structures/RoomView.js      | 13 ++++--
 src/components/structures/ScrollPanel.js   |  3 ++
 src/components/structures/TimelinePanel.js |  1 +
 src/utils/ResizeNotifier.js                | 52 ++++++++++++++++++++++
 10 files changed, 75 insertions(+), 6 deletions(-)
 create mode 100644 src/utils/ResizeNotifier.js

diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js
index 927449750c..e35a39a107 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.js
@@ -123,6 +123,7 @@ const FilePanel = React.createClass({
                     timelineSet={this.state.timelineSet}
                     showUrlPreview = {false}
                     tileShape="file_grid"
+                    resizeNotifier={this.props.resizeNotifier}
                     empty={_t('There are no visible files in this room')}
                 />
             );
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index c6c1be67ec..5267dba715 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -173,6 +173,7 @@ const LoggedInView = React.createClass({
             },
             onResized: (size) => {
                 window.localStorage.setItem("mx_lhs_size", '' + size);
+                this.props.resizeNotifier.notifyLeftHandleResized();
             },
         };
         const resizer = new Resizer(
@@ -448,6 +449,7 @@ const LoggedInView = React.createClass({
                         disabled={this.props.middleDisabled}
                         collapsedRhs={this.props.collapsedRhs}
                         ConferenceHandler={this.props.ConferenceHandler}
+                        resizeNotifier={this.props.resizeNotifier}
                     />;
                 break;
 
diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js
index 0427130eea..c0bf74d02c 100644
--- a/src/components/structures/MainSplit.js
+++ b/src/components/structures/MainSplit.js
@@ -27,6 +27,7 @@ export default class MainSplit extends React.Component {
 
     _onResized(size) {
         window.localStorage.setItem("mx_rhs_size", size);
+        this.props.resizeNotifier.notifyRightHandleResized();
     }
 
     _createResizer() {
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 2622a6bf93..a9b34c9058 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -48,6 +48,7 @@ import { _t, getCurrentLanguage } from '../../languageHandler';
 import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
 import { startAnyRegistrationFlow } from "../../Registration.js";
 import { messageForSyncError } from '../../utils/ErrorUtils';
+import ResizeNotifier from "../../utils/ResizeNotifier";
 
 const AutoDiscovery = Matrix.AutoDiscovery;
 
@@ -194,6 +195,7 @@ export default React.createClass({
             hideToSRUsers: false,
 
             syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
+            resizeNotifier: new ResizeNotifier(),
         };
         return s;
     },
@@ -1661,6 +1663,7 @@ export default React.createClass({
             dis.dispatch({ action: 'show_right_panel' });
         }
 
+        this.state.resizeNotifier.notifyWindowResized();
         this._windowWidth = window.innerWidth;
     },
 
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index b1f88a6221..aec2f8cbe1 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -703,7 +703,8 @@ module.exports = React.createClass({
                     onFillRequest={this.props.onFillRequest}
                     onUnfillRequest={this.props.onUnfillRequest}
                     style={style}
-                    stickyBottom={this.props.stickyBottom}>
+                    stickyBottom={this.props.stickyBottom}
+                    resizeNotifier={this.props.resizeNotifier}>
                 { topSpinner }
                 { this._getEventTiles() }
                 { whoIsTyping }
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index 5c745b04cc..93cbff3233 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -193,7 +193,7 @@ export default class RightPanel extends React.Component {
         } else if (this.state.phase === RightPanel.Phase.NotificationPanel) {
             panel = <NotificationPanel />;
         } else if (this.state.phase === RightPanel.Phase.FilePanel) {
-            panel = <FilePanel roomId={this.props.roomId} />;
+            panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
         }
 
         const classes = classNames("mx_RightPanel", "mx_fadable", {
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index d7488a2558..5a914cbd1c 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -392,7 +392,7 @@ module.exports = React.createClass({
         this._updateConfCallNotification();
 
         window.addEventListener('beforeunload', this.onPageUnload);
-        window.addEventListener('resize', this.onResize);
+        this.props.resizeNotifier.on("middlePanelResized", this.onResize);
         this.onResize();
 
         document.addEventListener("keydown", this.onKeyDown);
@@ -472,7 +472,7 @@ module.exports = React.createClass({
         }
 
         window.removeEventListener('beforeunload', this.onPageUnload);
-        window.removeEventListener('resize', this.onResize);
+        this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
 
         document.removeEventListener("keydown", this.onKeyDown);
 
@@ -1829,6 +1829,7 @@ module.exports = React.createClass({
                 className="mx_RoomView_messagePanel"
                 membersLoaded={this.state.membersLoaded}
                 permalinkCreator={this.state.permalinkCreator}
+                resizeNotifier={this.props.resizeNotifier}
             />);
 
         let topUnreadMessagesBar = null;
@@ -1861,7 +1862,7 @@ module.exports = React.createClass({
             },
         );
 
-        const rightPanel = this.state.room ? <RightPanel roomId={this.state.room.roomId} /> : undefined;
+        const rightPanel = this.state.room ? <RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} /> : undefined;
 
         return (
             <main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
@@ -1877,7 +1878,11 @@ module.exports = React.createClass({
                     onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
                     e2eStatus={this.state.e2eStatus}
                 />
-                <MainSplit panel={rightPanel} collapsedRhs={this.props.collapsedRhs}>
+                <MainSplit
+                    panel={rightPanel}
+                    collapsedRhs={this.props.collapsedRhs}
+                    resizeNotifier={this.props.resizeNotifier}
+                >
                     <div className={fadableSectionClasses}>
                         { auxPanel }
                         <div className="mx_RoomView_timeline">
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index cdb79686ad..799c88140e 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -149,6 +149,8 @@ module.exports = React.createClass({
 
     componentWillMount: function() {
         this._pendingFillRequests = {b: null, f: null};
+        this.props.resizeNotifier.on("middlePanelResized", this.onResize);
+
         this.resetScrollState();
     },
 
@@ -171,6 +173,7 @@ module.exports = React.createClass({
         //
         // (We could use isMounted(), but facebook have deprecated that.)
         this.unmounted = true;
+        this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
     },
 
     onScroll: function(ev) {
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 911499e314..537862ab1e 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -1227,6 +1227,7 @@ var TimelinePanel = React.createClass({
                           alwaysShowTimestamps={this.state.alwaysShowTimestamps}
                           className={this.props.className}
                           tileShape={this.props.tileShape}
+                          resizeNotifier={this.props.resizeNotifier}
             />
         );
     },
diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js
new file mode 100644
index 0000000000..43578ebcaa
--- /dev/null
+++ b/src/utils/ResizeNotifier.js
@@ -0,0 +1,52 @@
+/*
+Copyright 2019 New Vector 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.
+*/
+
+/**
+ * Fires when the middle panel has been resized.
+ * @event module:utils~ResizeNotifier#"middlePanelResized"
+ */
+import { EventEmitter } from "events";
+import { throttle } from "lodash";
+
+export default class ResizeNotifier extends EventEmitter {
+
+    constructor() {
+        super();
+        // with default options, will call fn once at first call, and then every x ms
+        // if there was another call in that timespan
+        this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
+    }
+
+    notifyBannersChanged() {
+        this.emit("middlePanelResized");
+    }
+
+    // can be called in quick succession
+    notifyLeftHandleResized() {
+        this._throttledMiddlePanel();
+    }
+
+    // can be called in quick succession
+    notifyRightHandleResized() {
+        this._throttledMiddlePanel();
+    }
+
+    // can be called in quick succession
+    notifyWindowResized() {
+        this._throttledMiddlePanel();
+    }
+}
+

From 56aeb5194a5e584c30b28e25abff3ab7b4fce33c Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Tue, 12 Mar 2019 16:37:20 +0100
Subject: [PATCH 04/10] emit timeline_resize in MatrixChat based on
 ResizeNotifier

as it's used in PersistentElement which could be used at various places
---
 src/components/structures/MatrixChat.js   | 8 ++++++++
 src/components/structures/MessagePanel.js | 5 -----
 src/components/structures/RoomView.js     | 5 -----
 3 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index a9b34c9058..6e05138e2d 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -318,6 +318,9 @@ export default React.createClass({
         // N.B. we don't call the whole of setTheme() here as we may be
         // racing with the theme CSS download finishing from index.js
         Tinter.tint();
+
+        // For PersistentElement
+        this.state.resizeNotifier.on("middlePanelResized", this._dispatchTimelineResize);
     },
 
     componentDidMount: function() {
@@ -400,6 +403,7 @@ export default React.createClass({
         dis.unregister(this.dispatcherRef);
         window.removeEventListener("focus", this.onFocus);
         window.removeEventListener('resize', this.handleResize);
+        this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize);
     },
 
     componentWillUpdate: function(props, state) {
@@ -1667,6 +1671,10 @@ export default React.createClass({
         this._windowWidth = window.innerWidth;
     },
 
+    _dispatchTimelineResize() {
+        dis.dispatch({ action: 'timeline_resize' }, true);
+    },
+
     onRoomCreated: function(roomId) {
         dis.dispatch({
             action: "view_room",
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index aec2f8cbe1..fecf5f1ad7 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -21,7 +21,6 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import shouldHideEvent from '../../shouldHideEvent';
 import {wantsDateSeparator} from '../../DateUtils';
-import dis from "../../dispatcher";
 import sdk from '../../index';
 
 import MatrixClientPeg from '../../MatrixClientPeg';
@@ -665,10 +664,6 @@ module.exports = React.createClass({
         }
     },
 
-    onResize: function() {
-        dis.dispatch({ action: 'timeline_resize' }, true);
-    },
-
     render: function() {
         const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
         const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 5a914cbd1c..507fbf6979 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -856,10 +856,6 @@ module.exports = React.createClass({
         }
     },
 
-    onSearchResultsResize: function() {
-        dis.dispatch({ action: 'timeline_resize' }, true);
-    },
-
     onSearchResultsFillRequest: function(backwards) {
         if (!backwards) {
             return Promise.resolve(false);
@@ -1794,7 +1790,6 @@ module.exports = React.createClass({
                     <ScrollPanel ref="searchResultsPanel"
                         className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
                         onFillRequest={this.onSearchResultsFillRequest}
-                        onResize={this.onSearchResultsResize}
                     >
                         <li className={scrollheader_classes}></li>
                         { this.getSearchResultTiles() }

From c8123ec66536c0e10e07c607b7a116d4a444ab6d Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Tue, 12 Mar 2019 16:38:30 +0100
Subject: [PATCH 05/10] use AutoHideScrollbar in memberlist

---
 res/css/views/rooms/_MemberList.scss     | 5 +++++
 src/components/views/rooms/MemberList.js | 7 ++++---
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index 9f2b5da930..cac97cb60d 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -20,6 +20,7 @@ limitations under the License.
     flex: 1;
     display: flex;
     flex-direction: column;
+    min-height: 0;
 
     .mx_Spinner {
         flex: 1 0 auto;
@@ -35,6 +36,10 @@ limitations under the License.
         margin-top: 8px;
         margin-bottom: 4px;
     }
+
+    .mx_AutoHideScrollbar {
+        flex: 1 1 0;
+    }
 }
 
 .mx_MemberList_chevron {
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index f9ce672c16..29f49f1691 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -20,6 +20,8 @@ import React from 'react';
 import { _t } from '../../../languageHandler';
 import SdkConfig from '../../../SdkConfig';
 import dis from '../../../dispatcher';
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+
 const MatrixClientPeg = require("../../../MatrixClientPeg");
 const sdk = require('../../../index');
 const rate_limited_func = require('../../../ratelimitedfunc');
@@ -439,7 +441,6 @@ module.exports = React.createClass({
 
         const SearchBox = sdk.getComponent('structures.SearchBox');
         const TruncatedList = sdk.getComponent("elements.TruncatedList");
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
 
         const cli = MatrixClientPeg.get();
         const room = cli.getRoom(this.props.roomId);
@@ -466,7 +467,7 @@ module.exports = React.createClass({
         return (
             <div className="mx_MemberList">
                 { inviteButton }
-                <GeminiScrollbarWrapper autoshow={true}>
+                <AutoHideScrollbar>
                     <div className="mx_MemberList_wrapper">
                         <TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
                             createOverflowElement={this._createOverflowTileJoined}
@@ -475,7 +476,7 @@ module.exports = React.createClass({
                         { invitedHeader }
                         { invitedSection }
                     </div>
-                </GeminiScrollbarWrapper>
+                </AutoHideScrollbar>
 
                 <SearchBox className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
                            placeholder={ _t('Filter room members') }

From d3af992d7f14d06555595464c3eff6c439165b43 Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Tue, 12 Mar 2019 16:38:42 +0100
Subject: [PATCH 06/10] use AutoHideScrollbar in member info panel

---
 src/components/views/rooms/MemberInfo.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 3ada730ec8..35161dedf7 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -44,6 +44,7 @@ import SdkConfig from '../../../SdkConfig';
 import MultiInviter from "../../../utils/MultiInviter";
 import SettingsStore from "../../../settings/SettingsStore";
 import E2EIcon from "./E2EIcon";
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 
 module.exports = withMatrixClient(React.createClass({
     displayName: 'MemberInfo',
@@ -1003,7 +1004,7 @@ module.exports = withMatrixClient(React.createClass({
                             { roomMemberDetails }
                         </div>
                     </div>
-                    <GeminiScrollbarWrapper autoshow={true} className="mx_MemberInfo_scrollContainer">
+                    <AutoHideScrollbar className="mx_MemberInfo_scrollContainer">
                         <div className="mx_MemberInfo_container">
                             { this._renderUserOptions() }
 
@@ -1015,7 +1016,7 @@ module.exports = withMatrixClient(React.createClass({
 
                             { spinner }
                         </div>
-                    </GeminiScrollbarWrapper>
+                    </AutoHideScrollbar>
             </div>
         );
     },

From 58f26ee9b070b1eca2943bdca5e14a96ca390a84 Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Tue, 12 Mar 2019 17:29:16 +0100
Subject: [PATCH 07/10] emit resize event when banners are shown/hidden to
 restore scroll pos

---
 src/Notifier.js                           | 12 +++++++++++-
 src/components/structures/LoggedInView.js | 19 +++++++++++++------
 src/components/structures/MatrixChat.js   | 13 ++++++++++---
 3 files changed, 34 insertions(+), 10 deletions(-)

diff --git a/src/Notifier.js b/src/Notifier.js
index 80e8be1084..ab8009c457 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -220,7 +220,17 @@ const Notifier = {
         }
     },
 
-    isToolbarHidden: function() {
+    shouldShowToolbar: function() {
+        const client = MatrixClientPeg.get();
+        if (!client) {
+            return false;
+        }
+        const isGuest = client.isGuest();
+        return !isGuest && Notifier.supportsDesktopNotifications() &&
+            !Notifier.isEnabled() && !Notifier._isToolbarHidden();
+    },
+
+    _isToolbarHidden: function() {
         // Check localStorage for any such meta data
         if (global.localStorage) {
             return global.localStorage.getItem("notifications_hidden") === "true";
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index 5267dba715..c22c217e5f 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -22,7 +22,6 @@ import PropTypes from 'prop-types';
 import { DragDropContext } from 'react-beautiful-dnd';
 
 import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
-import Notifier from '../../Notifier';
 import PageTypes from '../../PageTypes';
 import CallMediaHandler from '../../CallMediaHandler';
 import sdk from '../../index';
@@ -121,6 +120,18 @@ const LoggedInView = React.createClass({
         this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
     },
 
+    componentDidUpdate(prevProps) {
+        // attempt to guess when a banner was opened or closed
+        if (
+            (prevProps.showCookieBar !== this.props.showCookieBar) ||
+            (prevProps.hasNewVersion !== this.props.hasNewVersion) ||
+            (prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) ||
+            (prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
+        ) {
+            this.props.resizeNotifier.notifyBannersChanged();
+        }
+    },
+
     componentWillUnmount: function() {
         document.removeEventListener('keydown', this._onKeyDown);
         this._matrixClient.removeListener("accountData", this.onAccountData);
@@ -491,7 +502,6 @@ const LoggedInView = React.createClass({
         });
 
         let topBar;
-        const isGuest = this.props.matrixClient.isGuest();
         if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
             topBar = <ServerLimitBar kind='hard'
                 adminContact={this.state.syncErrorData.error.data.admin_contact}
@@ -515,10 +525,7 @@ const LoggedInView = React.createClass({
             topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
         } else if (this.state.userHasGeneratedPassword) {
             topBar = <PasswordNagBar />;
-        } else if (
-            !isGuest && Notifier.supportsDesktopNotifications() &&
-            !Notifier.isEnabled() && !Notifier.isToolbarHidden()
-        ) {
+        } else if (this.props.showNotifierToolbar) {
             topBar = <MatrixToolbar />;
         }
 
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 6e05138e2d..a7192b96cb 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -29,6 +29,7 @@ import PlatformPeg from "../../PlatformPeg";
 import SdkConfig from "../../SdkConfig";
 import * as RoomListSorter from "../../RoomListSorter";
 import dis from "../../dispatcher";
+import Notifier from '../../Notifier';
 
 import Modal from "../../Modal";
 import Tinter from "../../Tinter";
@@ -196,6 +197,7 @@ export default React.createClass({
 
             syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
             resizeNotifier: new ResizeNotifier(),
+            showNotifierToolbar: Notifier.shouldShowToolbar(),
         };
         return s;
     },
@@ -644,8 +646,9 @@ export default React.createClass({
             case 'view_invite':
                 showRoomInviteDialog(payload.roomId);
                 break;
-            case 'notifier_enabled':
-                this.forceUpdate();
+            case 'notifier_enabled': {
+                    this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
+                }
                 break;
             case 'hide_left_panel':
                 this.setState({
@@ -1180,6 +1183,7 @@ export default React.createClass({
      */
     _onLoggedIn: async function() {
         this.setStateForNewView({view: VIEWS.LOGGED_IN});
+        this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
         if (this._is_registered) {
             this._is_registered = false;
 
@@ -1672,7 +1676,10 @@ export default React.createClass({
     },
 
     _dispatchTimelineResize() {
-        dis.dispatch({ action: 'timeline_resize' }, true);
+        // prevent dispatch from within dispatch error
+        setTimeout(() => {
+            dis.dispatch({ action: 'timeline_resize' }, true);
+        }, 0);
     },
 
     onRoomCreated: function(roomId) {

From 9541cc175f357fc0cdeab5299239eac199d58b9a Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Tue, 12 Mar 2019 18:00:05 +0100
Subject: [PATCH 08/10] use ResizeNotifier as well to relayout room list

---
 src/components/structures/LeftPanel.js    |  2 +-
 src/components/structures/LoggedInView.js |  2 +-
 src/components/views/rooms/RoomList.js    | 10 ++++++----
 src/utils/ResizeNotifier.js               |  8 ++++++++
 4 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index 21438c597c..95b57a0ca5 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -234,7 +234,7 @@ const LeftPanel = React.createClass({
                     <CallPreview ConferenceHandler={VectorConferenceHandler} />
                     <RoomList
                         ref={this.collectRoomList}
-                        toolbarShown={this.props.toolbarShown}
+                        resizeNotifier={this.props.resizeNotifier}
                         collapsed={this.props.collapsed}
                         searchFilter={this.state.searchFilter}
                         ConferenceHandler={VectorConferenceHandler} />
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index c22c217e5f..4771c6f487 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -543,7 +543,7 @@ const LoggedInView = React.createClass({
                 <DragDropContext onDragEnd={this._onDragEnd}>
                     <div ref={this._setResizeContainerRef} className={bodyClasses}>
                         <LeftPanel
-                            toolbarShown={!!topBar}
+                            resizeNotifier={this.props.resizeNotifier}
                             collapsed={this.props.collapseLhs || false}
                             disabled={this.props.leftDisabled}
                         />
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 227dd318ed..2de9918e6e 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -212,7 +212,7 @@ module.exports = React.createClass({
         this._checkSubListsOverflow();
 
         this.resizer.attach();
-        window.addEventListener("resize", this.onWindowResize);
+        this.props.resizeNotifier.on("leftPanelResized", this.onResize);
         this.mounted = true;
     },
 
@@ -260,7 +260,6 @@ module.exports = React.createClass({
     componentWillUnmount: function() {
         this.mounted = false;
 
-        window.removeEventListener("resize", this.onWindowResize);
         dis.unregister(this.dispatcherRef);
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener("Room", this.onRoom);
@@ -272,6 +271,8 @@ module.exports = React.createClass({
             MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
             MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
         }
+        this.props.resizeNotifier.removeListener("leftPanelResized", this.onResize);
+
 
         if (this._tagStoreToken) {
             this._tagStoreToken.remove();
@@ -293,13 +294,14 @@ module.exports = React.createClass({
         this._delayedRefreshRoomList.cancelPendingCall();
     },
 
-    onWindowResize: function() {
+
+    onResize: function() {
         if (this.mounted && this._layout && this.resizeContainer &&
             Array.isArray(this._layoutSections)
         ) {
             this._layout.update(
                 this._layoutSections,
-                this.resizeContainer.offsetHeight
+                this.resizeContainer.offsetHeight,
             );
         }
     },
diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js
index 43578ebcaa..ff4b79091b 100644
--- a/src/utils/ResizeNotifier.js
+++ b/src/utils/ResizeNotifier.js
@@ -31,11 +31,13 @@ export default class ResizeNotifier extends EventEmitter {
     }
 
     notifyBannersChanged() {
+        this.emit("leftPanelResized");
         this.emit("middlePanelResized");
     }
 
     // can be called in quick succession
     notifyLeftHandleResized() {
+        // don't emit event for own region
         this._throttledMiddlePanel();
     }
 
@@ -46,6 +48,12 @@ export default class ResizeNotifier extends EventEmitter {
 
     // can be called in quick succession
     notifyWindowResized() {
+        // no need to throttle this one,
+        // also it could make scrollbars appear for
+        // a split second when the room list manual layout is now
+        // taller than the available space
+        this.emit("leftPanelResized");
+
         this._throttledMiddlePanel();
     }
 }

From 955ec14db98816dba3fe8a53de769cf11848df1e Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Thu, 14 Mar 2019 15:04:09 +0100
Subject: [PATCH 09/10] chrome apparently anchors the scroll position, which
 fights against our restore position logic. Disable it like this.

---
 res/css/structures/_RoomView.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 1b639928e0..ced0b9eab3 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -113,6 +113,7 @@ limitations under the License.
     width: 100%;
     overflow-y: auto;
     flex: 1 1 0;
+    overflow-anchor: none;
 }
 
 .mx_RoomView_messagePanelSearchSpinner {

From 0b8196343f008d9fd2e22cd54c2c3be85f7c5c8c Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Fri, 15 Mar 2019 09:57:26 +0100
Subject: [PATCH 10/10] fix some tests

---
 test/components/structures/MessagePanel-test.js | 9 ++++++++-
 test/components/structures/ScrollPanel-test.js  | 5 ++++-
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 0a51cb8918..c7287f1523 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -21,6 +21,7 @@ const ReactDOM = require("react-dom");
 const TestUtils = require('react-addons-test-utils');
 const expect = require('expect');
 import sinon from 'sinon';
+import { EventEmitter } from "events";
 
 const sdk = require('matrix-react-sdk');
 
@@ -46,8 +47,14 @@ const WrappedMessagePanel = React.createClass({
         };
     },
 
+    getInitialState: function() {
+        return {
+            resizeNotifier: new EventEmitter(),
+        };
+    },
+
     render: function() {
-        return <MessagePanel room={room} {...this.props} />;
+        return <MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} />;
     },
 });
 
diff --git a/test/components/structures/ScrollPanel-test.js b/test/components/structures/ScrollPanel-test.js
index 0e091cdddf..41d0f4469b 100644
--- a/test/components/structures/ScrollPanel-test.js
+++ b/test/components/structures/ScrollPanel-test.js
@@ -19,6 +19,7 @@ const ReactDOM = require("react-dom");
 const ReactTestUtils = require('react-addons-test-utils');
 const expect = require('expect');
 import Promise from 'bluebird';
+import { EventEmitter } from "events";
 
 const sdk = require('matrix-react-sdk');
 
@@ -29,6 +30,7 @@ const Tester = React.createClass({
     getInitialState: function() {
         return {
             tileKeys: [],
+            resizeNotifier: new EventEmitter(),
         };
     },
 
@@ -130,7 +132,8 @@ const Tester = React.createClass({
         return (
             <ScrollPanel ref="sp"
                 onScroll={this._onScroll}
-                onFillRequest={this._onFillRequest}>
+                onFillRequest={this._onFillRequest}
+                resizeNotifier={this.state.resizeNotifier}>
                     { tiles }
             </ScrollPanel>
         );