From 59b29e4a7f88d5ceca141e9b179310404c5fd54f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 27 Sep 2019 21:08:31 -0600
Subject: [PATCH 01/19] Add an option to hide image previews

Applies to images, stickers, and URL previews.

Fixes https://github.com/vector-im/riot-web/issues/10735
---
 res/css/views/messages/_MImageBody.scss       | 15 +++++
 res/css/views/messages/_MStickerBody.scss     | 11 ++++
 src/components/views/messages/MImageBody.js   | 62 ++++++++++++++-----
 src/components/views/messages/MStickerBody.js | 17 ++++-
 .../views/rooms/LinkPreviewWidget.js          |  4 ++
 .../settings/tabs/user/LabsUserSettingsTab.js |  1 +
 src/i18n/strings/en_EN.json                   |  3 +
 src/settings/Settings.js                      |  5 ++
 8 files changed, 102 insertions(+), 16 deletions(-)

diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss
index 86bc022829..e5c29ba9e0 100644
--- a/res/css/views/messages/_MImageBody.scss
+++ b/res/css/views/messages/_MImageBody.scss
@@ -19,6 +19,21 @@ limitations under the License.
     margin-right: 34px;
 }
 
+.mx_MImageBody_hidden {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    text-align: center;
+    border: 1px dashed $input-border-color;
+
+    // To center the text in the middle of the frame
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
 .mx_MImageBody_thumbnail {
     position: absolute;
     width: 100%;
diff --git a/res/css/views/messages/_MStickerBody.scss b/res/css/views/messages/_MStickerBody.scss
index e4977bcc34..162ee7da86 100644
--- a/res/css/views/messages/_MStickerBody.scss
+++ b/res/css/views/messages/_MStickerBody.scss
@@ -22,3 +22,14 @@ limitations under the License.
     position: absolute;
     top: 50%;
 }
+
+.mx_MStickerBody_hidden {
+    max-width: 220px;
+    text-decoration: none;
+    text-align: center;
+
+    // To center the text in the middle of the frame
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index de19d0026f..4f165ea7bd 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -64,6 +64,7 @@ export default class MImageBody extends React.Component {
             imgLoaded: false,
             loadedImageDimensions: null,
             hover: false,
+            showImage: SettingsStore.getValue("showImages"),
         };
     }
 
@@ -86,9 +87,19 @@ export default class MImageBody extends React.Component {
         }
     }
 
+    showImage() {
+        localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
+        this.setState({showImage: true});
+    }
+
     onClick(ev) {
         if (ev.button === 0 && !ev.metaKey) {
             ev.preventDefault();
+            if (!this.state.showImage) {
+                this.showImage();
+                return;
+            }
+
             const content = this.props.mxEvent.getContent();
             const httpUrl = this._getContentUrl();
             const ImageView = sdk.getComponent("elements.ImageView");
@@ -120,7 +131,7 @@ export default class MImageBody extends React.Component {
     onImageEnter(e) {
         this.setState({ hover: true });
 
-        if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
+        if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
             return;
         }
         const imgElement = e.target;
@@ -130,7 +141,7 @@ export default class MImageBody extends React.Component {
     onImageLeave(e) {
         this.setState({ hover: false });
 
-        if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
+        if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
             return;
         }
         const imgElement = e.target;
@@ -280,6 +291,12 @@ export default class MImageBody extends React.Component {
                 });
             }).done();
         }
+
+        // Remember that the user wanted to show this particular image
+        if (!this.state.showImage && localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true") {
+            this.setState({showImage: true});
+        }
+
         this._afterComponentDidMount();
     }
 
@@ -321,13 +338,21 @@ export default class MImageBody extends React.Component {
             // By doing this, the image "pops" into the timeline, but is still restricted
             // by the same width and height logic below.
             if (!this.state.loadedImageDimensions) {
-                return this.wrapImage(contentUrl,
+                let imageElement = (
                     <img style={{display: 'none'}} src={thumbUrl} ref="image"
-                        alt={content.body}
-                        onError={this.onImageError}
-                        onLoad={this.onImageLoad}
-                    />,
+                         alt={content.body}
+                         onError={this.onImageError}
+                         onLoad={this.onImageLoad}
+                    />
                 );
+                if (!this.state.showImage) {
+                    imageElement = (
+                        <div className="mx_MImageBody_hidden">
+                            <span>{_t("Click to show image")}</span>
+                        </div>
+                    );
+                }
+                return this.wrapImage(contentUrl, imageElement);
             }
             infoWidth = this.state.loadedImageDimensions.naturalWidth;
             infoHeight = this.state.loadedImageDimensions.naturalHeight;
@@ -362,13 +387,22 @@ export default class MImageBody extends React.Component {
             // Restrict the width of the thumbnail here, otherwise it will fill the container
             // which has the same width as the timeline
             // mx_MImageBody_thumbnail resizes img to exactly container size
-            img = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
-                style={{ maxWidth: maxWidth + "px" }}
-                alt={content.body}
-                onError={this.onImageError}
-                onLoad={this.onImageLoad}
-                onMouseEnter={this.onImageEnter}
-                onMouseLeave={this.onImageLeave} />;
+            img = (
+                <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
+                     style={{ maxWidth: maxWidth + "px" }}
+                     alt={content.body}
+                     onError={this.onImageError}
+                     onLoad={this.onImageLoad}
+                     onMouseEnter={this.onImageEnter}
+                     onMouseLeave={this.onImageLeave} />
+            );
+            if (!this.state.showImage) {
+                img = (
+                    <div style={{ maxWidth: maxWidth + "px" }} className="mx_MImageBody_hidden">
+                        <span>{_t("Click to show image")}</span>
+                    </div>
+                );
+            }
         }
 
         if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js
index 6a4128dfa7..c3e734a385 100644
--- a/src/components/views/messages/MStickerBody.js
+++ b/src/components/views/messages/MStickerBody.js
@@ -19,10 +19,15 @@ limitations under the License.
 import React from 'react';
 import MImageBody from './MImageBody';
 import sdk from '../../../index';
+import {_t} from "../../../languageHandler";
 
 export default class MStickerBody extends MImageBody {
-    // Empty to prevent default behaviour of MImageBody
-    onClick() {
+    // Mostly empty to prevent default behaviour of MImageBody
+    onClick(ev) {
+        ev.preventDefault();
+        if (!this.state.showImage) {
+            this.showImage();
+        }
     }
 
     // MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
@@ -34,6 +39,14 @@ export default class MStickerBody extends MImageBody {
     // Placeholder to show in place of the sticker image if
     // img onLoad hasn't fired yet.
     getPlaceholder() {
+        if (!this.state.showImage) {
+            return (
+                <a className="mx_MStickerBody_hidden" onClick={this.onClick} href="#">
+                    <span>{_t("Click to show sticker")}</span>
+                </a>
+            );
+        }
+
         const TintableSVG = sdk.getComponent('elements.TintableSvg');
         return <TintableSVG src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
     }
diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js
index cfa096a763..d93fe76b46 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.js
@@ -18,6 +18,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import createReactClass from 'create-react-class';
 import { linkifyElement } from '../../../HtmlUtils';
+import SettingsStore from "../../../settings/SettingsStore";
 
 const sdk = require('../../../index');
 const MatrixClientPeg = require('../../../MatrixClientPeg');
@@ -102,6 +103,9 @@ module.exports = createReactClass({
 
         // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
         let image = p["og:image"];
+        if (!SettingsStore.getValue("showImages")) {
+            image = null; // Don't render a button to show the image, just hide it outright
+        }
         const imageMaxWidth = 100; const imageMaxHeight = 100;
         if (image && image.startsWith("mxc://")) {
             image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
index 07a2bf722a..55bd821f95 100644
--- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
@@ -55,6 +55,7 @@ export default class LabsUserSettingsTab extends React.Component {
                     <SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
                     <SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
                     <SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
+                    <SettingsFlag name={"showImages"} level={SettingLevel.ACCOUNT} />
                 </div>
             </div>
         );
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index c23cd6d324..2f496eef00 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -369,6 +369,7 @@
     "Low bandwidth mode": "Low bandwidth mode",
     "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
     "Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
+    "Show previews/thumbnails for images": "Show previews/thumbnails for images",
     "Collecting app version information": "Collecting app version information",
     "Collecting logs": "Collecting logs",
     "Uploading report": "Uploading report",
@@ -1032,7 +1033,9 @@
     "Decrypt %(text)s": "Decrypt %(text)s",
     "Download %(text)s": "Download %(text)s",
     "Invalid file%(extra)s": "Invalid file%(extra)s",
+    "Click to show image": "Click to show image",
     "Error decrypting image": "Error decrypting image",
+    "Click to show sticker": "Click to show sticker",
     "Error decrypting video": "Error decrypting video",
     "Agree": "Agree",
     "Disagree": "Disagree",
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index e0ff16c538..efbbcf214f 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -409,4 +409,9 @@ export const SETTINGS = {
         ),
         default: true,
     },
+    "showImages": {
+        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+        displayName: _td("Show previews/thumbnails for images"),
+        default: true,
+    },
 };

From daef5f757422032804663a92b7737b8dcca4577a Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 1 Oct 2019 09:32:42 -0600
Subject: [PATCH 02/19] Move setting to real settings

---
 src/components/views/settings/tabs/user/LabsUserSettingsTab.js   | 1 -
 .../views/settings/tabs/user/PreferencesUserSettingsTab.js       | 1 +
 2 files changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
index 55bd821f95..07a2bf722a 100644
--- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
@@ -55,7 +55,6 @@ export default class LabsUserSettingsTab extends React.Component {
                     <SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
                     <SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
                     <SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
-                    <SettingsFlag name={"showImages"} level={SettingLevel.ACCOUNT} />
                 </div>
             </div>
         );
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index 6528c86f19..30f46754b7 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -43,6 +43,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'showJoinLeaves',
         'showAvatarChanges',
         'showDisplaynameChanges',
+        'showImages',
     ];
 
     static ROOM_LIST_SETTINGS = [

From 4b0596b6b776d2b99feae086c496c99ca9912777 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 1 Oct 2019 17:00:01 -0600
Subject: [PATCH 03/19] Apply lipstick to hidden image design

---
 res/css/views/messages/_MImageBody.scss       | 48 +++++++++++++------
 res/img/feather-customised/eye.svg            |  6 +++
 src/components/views/messages/MImageBody.js   | 39 ++++++++++-----
 src/components/views/messages/MStickerBody.js | 14 ++----
 src/i18n/strings/en_EN.json                   |  2 +-
 5 files changed, 71 insertions(+), 38 deletions(-)
 create mode 100644 res/img/feather-customised/eye.svg

diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss
index e5c29ba9e0..8e650eaff4 100644
--- a/res/css/views/messages/_MImageBody.scss
+++ b/res/css/views/messages/_MImageBody.scss
@@ -19,21 +19,6 @@ limitations under the License.
     margin-right: 34px;
 }
 
-.mx_MImageBody_hidden {
-    position: absolute;
-    left: 0;
-    top: 0;
-    bottom: 0;
-    right: 0;
-    text-align: center;
-    border: 1px dashed $input-border-color;
-
-    // To center the text in the middle of the frame
-    display: flex;
-    align-items: center;
-    justify-content: center;
-}
-
 .mx_MImageBody_thumbnail {
     position: absolute;
     width: 100%;
@@ -74,3 +59,36 @@ limitations under the License.
     color: $imagebody-giflabel-color;
     pointer-events: none;
 }
+
+.mx_HiddenImagePlaceholder {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+
+    // To center the text in the middle of the frame
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+
+    cursor: pointer;
+    background-color: $header-panel-bg-color;
+
+    .mx_HiddenImagePlaceholder_button {
+        color: $accent-color;
+
+        img {
+            margin-right: 8px;
+        }
+
+        span {
+            vertical-align: text-bottom;
+        }
+    }
+}
+
+.mx_EventTile:hover .mx_HiddenImagePlaceholder {
+    background-color: $primary-bg-color;
+}
diff --git a/res/img/feather-customised/eye.svg b/res/img/feather-customised/eye.svg
new file mode 100644
index 0000000000..fd06bf7b21
--- /dev/null
+++ b/res/img/feather-customised/eye.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 18 14">
+    <g fill="none" fill-rule="evenodd" stroke="#03B381" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)">
+        <path d="M0 6s3-6 8.25-6 8.25 6 8.25 6-3 6-8.25 6S0 6 0 6z"/>
+        <circle cx="8.25" cy="6" r="2.25"/>
+    </g>
+</svg>
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 4f165ea7bd..848cad6fb6 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -346,11 +346,7 @@ export default class MImageBody extends React.Component {
                     />
                 );
                 if (!this.state.showImage) {
-                    imageElement = (
-                        <div className="mx_MImageBody_hidden">
-                            <span>{_t("Click to show image")}</span>
-                        </div>
-                    );
+                    imageElement = <HiddenImagePlaceholder />;
                 }
                 return this.wrapImage(contentUrl, imageElement);
             }
@@ -381,7 +377,7 @@ export default class MImageBody extends React.Component {
             placeholder = this.getPlaceholder();
         }
 
-        const showPlaceholder = Boolean(placeholder);
+        let showPlaceholder = Boolean(placeholder);
 
         if (thumbUrl && !this.state.imgError) {
             // Restrict the width of the thumbnail here, otherwise it will fill the container
@@ -396,13 +392,11 @@ export default class MImageBody extends React.Component {
                      onMouseEnter={this.onImageEnter}
                      onMouseLeave={this.onImageLeave} />
             );
-            if (!this.state.showImage) {
-                img = (
-                    <div style={{ maxWidth: maxWidth + "px" }} className="mx_MImageBody_hidden">
-                        <span>{_t("Click to show image")}</span>
-                    </div>
-                );
-            }
+        }
+
+        if (!this.state.showImage) {
+            img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
+            showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon.
         }
 
         if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
@@ -488,3 +482,22 @@ export default class MImageBody extends React.Component {
         </span>;
     }
 }
+
+export class HiddenImagePlaceholder extends React.PureComponent {
+    static propTypes = {
+        hover: PropTypes.bool,
+    };
+
+    render() {
+        let className = 'mx_HiddenImagePlaceholder';
+        if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
+        return (
+            <div className={className}>
+                <div className='mx_HiddenImagePlaceholder_button'>
+                    <img src={require("../../../../res/img/feather-customised/eye.svg")} width={17} height={12} />
+                    <span>{_t("Show image")}</span>
+                </div>
+            </div>
+        );
+    }
+}
diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js
index c3e734a385..1bd1c1f2eb 100644
--- a/src/components/views/messages/MStickerBody.js
+++ b/src/components/views/messages/MStickerBody.js
@@ -33,20 +33,16 @@ export default class MStickerBody extends MImageBody {
     // MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
     // which is added by mx_MStickerBody_wrapper
     wrapImage(contentUrl, children) {
-        return <div className="mx_MStickerBody_wrapper"> { children } </div>;
+        let onClick = null;
+        if (!this.state.showImage) {
+            onClick = this.onClick;
+        }
+        return <div className="mx_MStickerBody_wrapper" onClick={onClick}> { children } </div>;
     }
 
     // Placeholder to show in place of the sticker image if
     // img onLoad hasn't fired yet.
     getPlaceholder() {
-        if (!this.state.showImage) {
-            return (
-                <a className="mx_MStickerBody_hidden" onClick={this.onClick} href="#">
-                    <span>{_t("Click to show sticker")}</span>
-                </a>
-            );
-        }
-
         const TintableSVG = sdk.getComponent('elements.TintableSvg');
         return <TintableSVG src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
     }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4844d409bc..9fa2546bad 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1036,8 +1036,8 @@
     "Decrypt %(text)s": "Decrypt %(text)s",
     "Download %(text)s": "Download %(text)s",
     "Invalid file%(extra)s": "Invalid file%(extra)s",
-    "Click to show image": "Click to show image",
     "Error decrypting image": "Error decrypting image",
+    "Show image": "Show image",
     "Click to show sticker": "Click to show sticker",
     "Error decrypting video": "Error decrypting video",
     "Agree": "Agree",

From cf4fa068c445be2e8e309f794897d4ea2740c374 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 1 Oct 2019 17:06:21 -0600
Subject: [PATCH 04/19] Remove old string

---
 src/i18n/strings/en_EN.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9fa2546bad..43d6bd570d 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1038,7 +1038,6 @@
     "Invalid file%(extra)s": "Invalid file%(extra)s",
     "Error decrypting image": "Error decrypting image",
     "Show image": "Show image",
-    "Click to show sticker": "Click to show sticker",
     "Error decrypting video": "Error decrypting video",
     "Agree": "Agree",
     "Disagree": "Disagree",

From a719623bb99ef7d4fd0cbe3b16e63827c7f43d60 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 1 Oct 2019 17:06:55 -0600
Subject: [PATCH 05/19] Appease the linter

---
 src/components/views/messages/MStickerBody.js | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js
index 1bd1c1f2eb..ed82d49576 100644
--- a/src/components/views/messages/MStickerBody.js
+++ b/src/components/views/messages/MStickerBody.js
@@ -14,12 +14,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-'use strict';
-
 import React from 'react';
 import MImageBody from './MImageBody';
 import sdk from '../../../index';
-import {_t} from "../../../languageHandler";
 
 export default class MStickerBody extends MImageBody {
     // Mostly empty to prevent default behaviour of MImageBody

From 4bd72b14949b52d1aae3ee68100e25a098c77328 Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Wed, 2 Oct 2019 09:53:55 +0200
Subject: [PATCH 06/19] dont let placeholder show scrollbar when composer is
 narrow

---
 res/css/views/rooms/_BasicMessageComposer.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss
index b32a44219a..ce519b1ea7 100644
--- a/res/css/views/rooms/_BasicMessageComposer.scss
+++ b/res/css/views/rooms/_BasicMessageComposer.scss
@@ -42,7 +42,7 @@ limitations under the License.
         white-space: pre-wrap;
         word-wrap: break-word;
         outline: none;
-        overflow-x: auto;
+        overflow-x: hidden;
 
         span.mx_UserPill, span.mx_RoomPill {
             padding-left: 21px;

From 55c1c5e5828496b8465a7d4ada2680e44d5d191b Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Wed, 2 Oct 2019 14:31:42 +0200
Subject: [PATCH 07/19] tell tooltip when format bar gets hidden, as it won't
 be unmounted

---
 .../views/elements/InteractiveTooltip.js      |  4 ++-
 .../views/rooms/MessageComposerFormatBar.js   | 29 ++++++++++++-------
 2 files changed, 22 insertions(+), 11 deletions(-)

diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js
index 41d66ae629..bcad8bc6bb 100644
--- a/src/components/views/elements/InteractiveTooltip.js
+++ b/src/components/views/elements/InteractiveTooltip.js
@@ -95,6 +95,8 @@ export default class InteractiveTooltip extends React.Component {
         content: PropTypes.node.isRequired,
         // Function to call when visibility of the tooltip changes
         onVisibilityChange: PropTypes.func,
+        // flag to forcefully hide this tooltip
+        forceHidden: PropTypes.bool,
     };
 
     constructor() {
@@ -269,8 +271,8 @@ export default class InteractiveTooltip extends React.Component {
 
     renderTooltip() {
         const { contentRect, visible } = this.state;
-        if (!visible) {
             ReactDOM.unmountComponentAtNode(getOrCreateContainer());
+        if (this.props.forceHidden === true || !visible) {
             return null;
         }
 
diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js
index 8090fb2ad5..95c896c6fc 100644
--- a/src/components/views/rooms/MessageComposerFormatBar.js
+++ b/src/components/views/rooms/MessageComposerFormatBar.js
@@ -18,7 +18,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import sdk from '../../../index';
-
+import classNames from 'classnames';
 
 export default class MessageComposerFormatBar extends React.PureComponent {
     static propTypes = {
@@ -26,18 +26,26 @@ export default class MessageComposerFormatBar extends React.PureComponent {
         shortcuts: PropTypes.object.isRequired,
     }
 
+    constructor(props) {
+        super(props);
+        this.state = {visible: false};
+    }
+
     render() {
-        return (<div className="mx_MessageComposerFormatBar" ref={ref => this._formatBarRef = ref}>
-            <FormatButton shortcut={this.props.shortcuts.bold} label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" />
-            <FormatButton shortcut={this.props.shortcuts.italics} label={_t("Italics")} onClick={() => this.props.onAction("italics")} icon="Italic" />
-            <FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction("strikethrough")} icon="Strikethrough" />
-            <FormatButton label={_t("Code block")} onClick={() => this.props.onAction("code")} icon="Code" />
-            <FormatButton shortcut={this.props.shortcuts.quote} label={_t("Quote")} onClick={() => this.props.onAction("quote")} icon="Quote" />
+        const classes = classNames("mx_MessageComposerFormatBar", {
+            "mx_MessageComposerFormatBar_shown": this.state.visible,
+        });
+        return (<div className={classes} ref={ref => this._formatBarRef = ref}>
+            <FormatButton label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
+            <FormatButton label={_t("Italics")} onClick={() => this.props.onAction("italics")} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} />
+            <FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction("strikethrough")} icon="Strikethrough" visible={this.state.visible} />
+            <FormatButton label={_t("Code block")} onClick={() => this.props.onAction("code")} icon="Code" visible={this.state.visible} />
+            <FormatButton label={_t("Quote")} onClick={() => this.props.onAction("quote")} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
         </div>);
     }
 
     showAt(selectionRect) {
-        this._formatBarRef.classList.add("mx_MessageComposerFormatBar_shown");
+        this.setState({visible: true});
         const parentRect = this._formatBarRef.parentElement.getBoundingClientRect();
         this._formatBarRef.style.left = `${selectionRect.left - parentRect.left}px`;
         // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok.
@@ -45,7 +53,7 @@ export default class MessageComposerFormatBar extends React.PureComponent {
     }
 
     hide() {
-        this._formatBarRef.classList.remove("mx_MessageComposerFormatBar_shown");
+        this.setState({visible: false});
     }
 }
 
@@ -55,6 +63,7 @@ class FormatButton extends React.PureComponent {
         onClick: PropTypes.func.isRequired,
         icon: PropTypes.string.isRequired,
         shortcut: PropTypes.string,
+        visible: PropTypes.bool,
     }
 
     render() {
@@ -72,7 +81,7 @@ class FormatButton extends React.PureComponent {
         );
 
         return (
-            <InteractiveTooltip content={tooltipContent}>
+            <InteractiveTooltip content={tooltipContent} forceHidden={!this.props.visible}>
                 <span aria-label={this.props.label}
                    role="button"
                    onClick={this.props.onClick}

From 44e68f16bab94f62cc479bb23007b77773cb817e Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Wed, 2 Oct 2019 14:32:08 +0200
Subject: [PATCH 08/19] use a way of unmounting the tooltip that seems to work
 100% of the time
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

¯\_(ツ)_/¯
---
 src/components/views/elements/InteractiveTooltip.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js
index bcad8bc6bb..0bb356c5ba 100644
--- a/src/components/views/elements/InteractiveTooltip.js
+++ b/src/components/views/elements/InteractiveTooltip.js
@@ -271,8 +271,8 @@ export default class InteractiveTooltip extends React.Component {
 
     renderTooltip() {
         const { contentRect, visible } = this.state;
-            ReactDOM.unmountComponentAtNode(getOrCreateContainer());
         if (this.props.forceHidden === true || !visible) {
+            ReactDOM.render(null, getOrCreateContainer());
             return null;
         }
 

From d82a05fbae4603d70b36daa6e2e24eb511ffbfb8 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Wed, 2 Oct 2019 15:26:54 +0100
Subject: [PATCH 09/19] Fix integration manager not updating when set

setUserWidget was modifying the content of the old event itself,
so when `waitForUserWidget()` checked the content to see if it was
there yet, it was, but because the echo hadn't come back, the
IntegrationManager hadn't rebuilt its list.

In other news, its terrifying that we can just accidentally modify
the content of an event in the store. I'm going to make a js-sdk
PR that freezes the content and see what breaks...

Fixes https://github.com/vector-im/riot-web/issues/10977
---
 src/utils/WidgetUtils.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index 237db82365..36907da5ab 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -233,7 +233,9 @@ export default class WidgetUtils {
         };
 
         const client = MatrixClientPeg.get();
-        const userWidgets = WidgetUtils.getUserWidgets();
+        // Get the current widgets and clone them before we modify them, otherwise
+        // we'll modify the content of the old event.
+        const userWidgets = JSON.parse(JSON.stringify(WidgetUtils.getUserWidgets()));
 
         // Delete existing widget with ID
         try {

From 4136e0722d2f25aa29152f6e2f7cb805da899e2b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 2 Oct 2019 09:34:30 -0600
Subject: [PATCH 10/19] Add a bit of debugging for clear cache & reload button

---
 src/components/views/settings/tabs/user/HelpUserSettingsTab.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
index a50b26fed6..683cf90222 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
@@ -71,6 +71,9 @@ export default class HelpUserSettingsTab extends React.Component {
     _onClearCacheAndReload = (e) => {
         if (!PlatformPeg.get()) return;
 
+        // Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly
+        // stopping in the middle of the logs.
+        console.log("Clear cache & reload clicked");
         MatrixClientPeg.get().stopClient();
         MatrixClientPeg.get().store.deleteAllData().done(() => {
             PlatformPeg.get().reload();

From d97ff8a692c4b61f794ac90e72c5aec9a93f1f53 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 2 Oct 2019 09:45:53 -0600
Subject: [PATCH 11/19] Add debugging for soft logout too

---
 src/Lifecycle.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index c03a958840..a2cfc9a1ba 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -544,6 +544,9 @@ export function softLogout() {
     // been soft logged out, despite having credentials and data for a MatrixClient).
     localStorage.setItem("mx_soft_logout", "true");
 
+    // Dev note: please keep this log line around. It can be useful for track down
+    // random clients stopping in the middle of the logs.
+    console.log("Soft logout initiated");
     _isLoggingOut = true; // to avoid repeated flags
     stopMatrixClient(/*unsetClient=*/false);
     dis.dispatch({action: 'on_client_not_viable'}); // generic version of on_logged_out

From 0e8dc24c3ff6c78f08b5101b260bdee1cc02b956 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 2 Oct 2019 16:26:23 +0100
Subject: [PATCH 12/19] Add a basic error boundary for the entire app

This adds a basic error boundary around the entire app to catch errors during
rendering and present the user with the options on how to proceed. This is not
implemented as a modal so that it could be used selectively in portions of the
app as well, such as just the `RoomView`.

Fixes https://github.com/vector-im/riot-web/issues/11009
---
 res/css/_components.scss                      |   1 +
 res/css/views/elements/_ErrorBoundary.scss    |  34 ++++++
 src/components/structures/MatrixChat.js       |  62 +++++------
 .../views/elements/ErrorBoundary.js           | 104 ++++++++++++++++++
 .../settings/tabs/user/HelpUserSettingsTab.js |   2 +-
 src/i18n/strings/en_EN.json                   |   3 +-
 6 files changed, 169 insertions(+), 37 deletions(-)
 create mode 100644 res/css/views/elements/_ErrorBoundary.scss
 create mode 100644 src/components/views/elements/ErrorBoundary.js

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 40a797dc15..f627fe3a29 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -87,6 +87,7 @@
 @import "./views/elements/_DirectorySearchBox.scss";
 @import "./views/elements/_Dropdown.scss";
 @import "./views/elements/_EditableItemList.scss";
+@import "./views/elements/_ErrorBoundary.scss";
 @import "./views/elements/_Field.scss";
 @import "./views/elements/_ImageView.scss";
 @import "./views/elements/_InlineSpinner.scss";
diff --git a/res/css/views/elements/_ErrorBoundary.scss b/res/css/views/elements/_ErrorBoundary.scss
new file mode 100644
index 0000000000..e46ba69a7c
--- /dev/null
+++ b/res/css/views/elements/_ErrorBoundary.scss
@@ -0,0 +1,34 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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.
+*/
+
+.mx_ErrorBoundary {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.mx_ErrorBoundary_body {
+    display: flex;
+    flex-direction: column;
+    max-width: 400px;
+    align-items: center;
+
+    .mx_AccessibleButton {
+        margin-top: 5px;
+    }
+}
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 2da219a28d..da67416400 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -1808,28 +1808,26 @@ export default createReactClass({
     render: function() {
         // console.log(`Rendering MatrixChat with view ${this.state.view}`);
 
+        let view;
+
         if (
             this.state.view === VIEWS.LOADING ||
             this.state.view === VIEWS.LOGGING_IN
         ) {
             const Spinner = sdk.getComponent('elements.Spinner');
-            return (
+            view = (
                 <div className="mx_MatrixChat_splash">
                     <Spinner />
                 </div>
             );
-        }
-
-        // needs to be before normal PageTypes as you are logged in technically
-        if (this.state.view === VIEWS.POST_REGISTRATION) {
+        } else if (this.state.view === VIEWS.POST_REGISTRATION) {
+            // needs to be before normal PageTypes as you are logged in technically
             const PostRegistration = sdk.getComponent('structures.auth.PostRegistration');
-            return (
+            view = (
                 <PostRegistration
                     onComplete={this.onFinishPostRegistration} />
             );
-        }
-
-        if (this.state.view === VIEWS.LOGGED_IN) {
+        } else if (this.state.view === VIEWS.LOGGED_IN) {
             // store errors stop the client syncing and require user intervention, so we'll
             // be showing a dialog. Don't show anything else.
             const isStoreError = this.state.syncError && this.state.syncError instanceof Matrix.InvalidStoreError;
@@ -1843,8 +1841,8 @@ export default createReactClass({
                  * as using something like redux to avoid having a billion bits of state kicking around.
                  */
                 const LoggedInView = sdk.getComponent('structures.LoggedInView');
-                return (
-                   <LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
+                view = (
+                    <LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
                         onRoomCreated={this.onRoomCreated}
                         onCloseAllSettings={this.onCloseAllSettings}
                         onRegistered={this.onRegistered}
@@ -1863,26 +1861,22 @@ export default createReactClass({
                         {messageForSyncError(this.state.syncError)}
                     </div>;
                 }
-                return (
+                view = (
                     <div className="mx_MatrixChat_splash">
                         {errorBox}
                         <Spinner />
                         <a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
-                        { _t('Logout') }
+                            {_t('Logout')}
                         </a>
                     </div>
                 );
             }
-        }
-
-        if (this.state.view === VIEWS.WELCOME) {
+        } else if (this.state.view === VIEWS.WELCOME) {
             const Welcome = sdk.getComponent('auth.Welcome');
-            return <Welcome />;
-        }
-
-        if (this.state.view === VIEWS.REGISTER) {
+            view = <Welcome />;
+        } else if (this.state.view === VIEWS.REGISTER) {
             const Registration = sdk.getComponent('structures.auth.Registration');
-            return (
+            view = (
                 <Registration
                     clientSecret={this.state.register_client_secret}
                     sessionId={this.state.register_session_id}
@@ -1896,12 +1890,9 @@ export default createReactClass({
                     {...this.getServerProperties()}
                 />
             );
-        }
-
-
-        if (this.state.view === VIEWS.FORGOT_PASSWORD) {
+        } else if (this.state.view === VIEWS.FORGOT_PASSWORD) {
             const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
-            return (
+            view = (
                 <ForgotPassword
                     onComplete={this.onLoginClick}
                     onLoginClick={this.onLoginClick}
@@ -1909,11 +1900,9 @@ export default createReactClass({
                     {...this.getServerProperties()}
                 />
             );
-        }
-
-        if (this.state.view === VIEWS.LOGIN) {
+        } else if (this.state.view === VIEWS.LOGIN) {
             const Login = sdk.getComponent('structures.auth.Login');
-            return (
+            view = (
                 <Login
                     onLoggedIn={Lifecycle.setLoggedIn}
                     onRegisterClick={this.onRegisterClick}
@@ -1924,18 +1913,21 @@ export default createReactClass({
                     {...this.getServerProperties()}
                 />
             );
-        }
-
-        if (this.state.view === VIEWS.SOFT_LOGOUT) {
+        } else if (this.state.view === VIEWS.SOFT_LOGOUT) {
             const SoftLogout = sdk.getComponent('structures.auth.SoftLogout');
-            return (
+            view = (
                 <SoftLogout
                     realQueryParams={this.props.realQueryParams}
                     onTokenLoginCompleted={this.props.onTokenLoginCompleted}
                 />
             );
+        } else {
+            console.error(`Unknown view ${this.state.view}`);
         }
 
-        console.error(`Unknown view ${this.state.view}`);
+        const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
+        return <ErrorBoundary>
+            {view}
+        </ErrorBoundary>;
     },
 });
diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js
new file mode 100644
index 0000000000..630b369caa
--- /dev/null
+++ b/src/components/views/elements/ErrorBoundary.js
@@ -0,0 +1,104 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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 sdk from '../../../index';
+import { _t } from '../../../languageHandler';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import PlatformPeg from '../../../PlatformPeg';
+import Modal from '../../../Modal';
+
+/**
+ * This error boundary component can be used to wrap large content areas and
+ * catch exceptions during rendering in the component tree below them.
+ */
+export default class ErrorBoundary extends React.PureComponent {
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            error: null,
+        };
+    }
+
+    static getDerivedStateFromError(error) {
+        // Side effects are not permitted here, so we only update the state so
+        // that the next render shows an error message.
+        return { error };
+    }
+
+    componentDidCatch(error, { componentStack }) {
+        // Browser consoles are better at formatting output when native errors are passed
+        // in their own `console.error` invocation.
+        console.error(error);
+        console.error(
+            "The above error occured while React was rendering the following components:",
+            componentStack,
+        );
+    }
+
+    _onClearCacheAndReload = () => {
+        if (!PlatformPeg.get()) return;
+
+        MatrixClientPeg.get().stopClient();
+        MatrixClientPeg.get().store.deleteAllData().done(() => {
+            PlatformPeg.get().reload();
+        });
+    };
+
+    _onBugReport = () => {
+        const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
+        if (!BugReportDialog) {
+            return;
+        }
+        Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
+    };
+
+    render() {
+        if (this.state.error) {
+            const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
+            const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new";
+            return <div className="mx_ErrorBoundary">
+                <div className="mx_ErrorBoundary_body">
+                    <h1>{_t("Something went wrong!")}</h1>
+                    <p>{_t(
+                        "Please <newIssueLink>create a new issue</newIssueLink> " +
+                        "on GitHub so that we can investigate this bug.", {}, {
+                            newIssueLink: (sub) => {
+                                return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
+                            },
+                        },
+                    )}</p>
+                    <p>{_t(
+                        "If you've submitted a bug via GitHub, debug logs can help " +
+                        "us track down the problem. Debug logs contain application " +
+                        "usage data including your username, the IDs or aliases of " +
+                        "the rooms or groups you have visited and the usernames of " +
+                        "other users. They do not contain messages.",
+                    )}</p>
+                    <AccessibleButton onClick={this._onBugReport} kind='primary'>
+                        {_t("Submit debug logs")}
+                    </AccessibleButton>
+                    <AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
+                        {_t("Clear cache and reload")}
+                    </AccessibleButton>
+                </div>
+            </div>;
+        }
+
+        return this.props.children;
+    }
+}
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
index a50b26fed6..ccabee3ca8 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
@@ -226,7 +226,7 @@ export default class HelpUserSettingsTab extends React.Component {
                         </div>
                         <div className='mx_HelpUserSettingsTab_debugButton'>
                             <AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
-                                {_t("Clear Cache and Reload")}
+                                {_t("Clear cache and reload")}
                             </AccessibleButton>
                         </div>
                     </div>
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5a683ce92d..4f25c25ec2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -617,7 +617,7 @@
     "Bug reporting": "Bug reporting",
     "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
     "Submit debug logs": "Submit debug logs",
-    "Clear Cache and Reload": "Clear Cache and Reload",
+    "Clear cache and reload": "Clear cache and reload",
     "FAQ": "FAQ",
     "Versions": "Versions",
     "matrix-react-sdk version:": "matrix-react-sdk version:",
@@ -1124,6 +1124,7 @@
     "No results": "No results",
     "Yes": "Yes",
     "No": "No",
+    "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
     "Communities": "Communities",
     "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
     "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s",

From b605c0048d0e44c1f573466764fe778fa7baf2a7 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 2 Oct 2019 17:28:03 +0100
Subject: [PATCH 13/19] Add an error boundary around the RoomView

This adds a more specific boundary around the `RoomView` for room-specific
errors and is an example how we could use add boundaries around just a portion
of the app.
---
 src/components/structures/RoomView.js | 135 ++++++++++++++------------
 1 file changed, 73 insertions(+), 62 deletions(-)

diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 4d52158dae..13523104ae 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -1566,20 +1566,23 @@ module.exports = createReactClass({
         const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
         const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
         const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
+        const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
 
         if (!this.state.room) {
             const loading = this.state.roomLoading || this.state.peekLoading;
             if (loading) {
                 return (
                     <div className="mx_RoomView">
-                        <RoomPreviewBar
-                            canPreview={false}
-                            previewLoading={this.state.peekLoading}
-                            error={this.state.roomLoadError}
-                            loading={loading}
-                            joining={this.state.joining}
-                            oobData={this.props.oobData}
-                        />
+                        <ErrorBoundary>
+                            <RoomPreviewBar
+                                canPreview={false}
+                                previewLoading={this.state.peekLoading}
+                                error={this.state.roomLoadError}
+                                loading={loading}
+                                joining={this.state.joining}
+                                oobData={this.props.oobData}
+                            />
+                        </ErrorBoundary>
                     </div>
                 );
             } else {
@@ -1597,18 +1600,20 @@ module.exports = createReactClass({
                 const roomAlias = this.state.roomAlias;
                 return (
                     <div className="mx_RoomView">
-                        <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
-                            onForgetClick={this.onForgetClick}
-                            onRejectClick={this.onRejectThreepidInviteButtonClicked}
-                            canPreview={false} error={this.state.roomLoadError}
-                            roomAlias={roomAlias}
-                            joining={this.state.joining}
-                            inviterName={inviterName}
-                            invitedEmail={invitedEmail}
-                            oobData={this.props.oobData}
-                            signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null}
-                            room={this.state.room}
-                        />
+                        <ErrorBoundary>
+                            <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
+                                onForgetClick={this.onForgetClick}
+                                onRejectClick={this.onRejectThreepidInviteButtonClicked}
+                                canPreview={false} error={this.state.roomLoadError}
+                                roomAlias={roomAlias}
+                                joining={this.state.joining}
+                                inviterName={inviterName}
+                                invitedEmail={invitedEmail}
+                                oobData={this.props.oobData}
+                                signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null}
+                                room={this.state.room}
+                            />
+                        </ErrorBoundary>
                     </div>
                 );
             }
@@ -1618,12 +1623,14 @@ module.exports = createReactClass({
         if (myMembership == 'invite') {
             if (this.state.joining || this.state.rejecting) {
                 return (
-                    <RoomPreviewBar
+                    <ErrorBoundary>
+                        <RoomPreviewBar
                             canPreview={false}
                             error={this.state.roomLoadError}
                             joining={this.state.joining}
                             rejecting={this.state.rejecting}
                         />
+                    </ErrorBoundary>
                 );
             } else {
                 const myUserId = MatrixClientPeg.get().credentials.userId;
@@ -1638,14 +1645,16 @@ module.exports = createReactClass({
                 // We have a regular invite for this room.
                 return (
                     <div className="mx_RoomView">
-                        <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
-                            onForgetClick={this.onForgetClick}
-                            onRejectClick={this.onRejectButtonClicked}
-                            inviterName={inviterName}
-                            canPreview={false}
-                            joining={this.state.joining}
-                            room={this.state.room}
-                        />
+                        <ErrorBoundary>
+                            <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
+                                onForgetClick={this.onForgetClick}
+                                onRejectClick={this.onRejectButtonClicked}
+                                inviterName={inviterName}
+                                canPreview={false}
+                                joining={this.state.joining}
+                                room={this.state.room}
+                            />
+                        </ErrorBoundary>
                     </div>
                 );
             }
@@ -1942,41 +1951,43 @@ module.exports = createReactClass({
 
         return (
             <main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
-                <RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
-                    oobData={this.props.oobData}
-                    inRoom={myMembership === 'join'}
-                    collapsedRhs={collapsedRhs}
-                    onSearchClick={this.onSearchClick}
-                    onSettingsClick={this.onSettingsClick}
-                    onPinnedClick={this.onPinnedClick}
-                    onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
-                    onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
-                    onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
-                    e2eStatus={this.state.e2eStatus}
-                />
-                <MainSplit
-                    panel={rightPanel}
-                    collapsedRhs={collapsedRhs}
-                    resizeNotifier={this.props.resizeNotifier}
-                >
-                    <div className={fadableSectionClasses}>
-                        { auxPanel }
-                        <div className="mx_RoomView_timeline">
-                            { topUnreadMessagesBar }
-                            { jumpToBottom }
-                            { messagePanel }
-                            { searchResultsPanel }
-                        </div>
-                        <div className={statusBarAreaClass}>
-                            <div className="mx_RoomView_statusAreaBox">
-                                <div className="mx_RoomView_statusAreaBox_line"></div>
-                                { statusBar }
+                <ErrorBoundary>
+                    <RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
+                        oobData={this.props.oobData}
+                        inRoom={myMembership === 'join'}
+                        collapsedRhs={collapsedRhs}
+                        onSearchClick={this.onSearchClick}
+                        onSettingsClick={this.onSettingsClick}
+                        onPinnedClick={this.onPinnedClick}
+                        onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
+                        onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
+                        onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
+                        e2eStatus={this.state.e2eStatus}
+                    />
+                    <MainSplit
+                        panel={rightPanel}
+                        collapsedRhs={collapsedRhs}
+                        resizeNotifier={this.props.resizeNotifier}
+                    >
+                        <div className={fadableSectionClasses}>
+                            {auxPanel}
+                            <div className="mx_RoomView_timeline">
+                                {topUnreadMessagesBar}
+                                {jumpToBottom}
+                                {messagePanel}
+                                {searchResultsPanel}
                             </div>
+                            <div className={statusBarAreaClass}>
+                                <div className="mx_RoomView_statusAreaBox">
+                                    <div className="mx_RoomView_statusAreaBox_line"></div>
+                                    {statusBar}
+                                </div>
+                            </div>
+                            {previewBar}
+                            {messageComposer}
                         </div>
-                        { previewBar }
-                        { messageComposer }
-                    </div>
-                </MainSplit>
+                    </MainSplit>
+                </ErrorBoundary>
             </main>
         );
     },

From 37d16db0f083005c8beb5c7d5b6dfd3e0bd25361 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 2 Oct 2019 12:46:21 -0600
Subject: [PATCH 14/19] Elsify

---
 src/components/views/messages/MImageBody.js | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 848cad6fb6..e89c6d2bc9 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -338,15 +338,17 @@ export default class MImageBody extends React.Component {
             // By doing this, the image "pops" into the timeline, but is still restricted
             // by the same width and height logic below.
             if (!this.state.loadedImageDimensions) {
-                let imageElement = (
-                    <img style={{display: 'none'}} src={thumbUrl} ref="image"
-                         alt={content.body}
-                         onError={this.onImageError}
-                         onLoad={this.onImageLoad}
-                    />
-                );
+                let imageElement;
                 if (!this.state.showImage) {
                     imageElement = <HiddenImagePlaceholder />;
+                } else {
+                    imageElement = (
+                        <img style={{display: 'none'}} src={thumbUrl} ref="image"
+                             alt={content.body}
+                             onError={this.onImageError}
+                             onLoad={this.onImageLoad}
+                        />
+                    );
                 }
                 return this.wrapImage(contentUrl, imageElement);
             }

From f1db0cf027a8dc3c1d3065ae8de40d9beecd50e3 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Oct 2019 09:35:39 +0100
Subject: [PATCH 15/19] Various ARIA a11y fixes.

Notate RightPanel tabs.
Shorten Screen Reader queues.
Make AccessibleTooltipButton screen reader friendly
Flatten DOM for Sticker button using React Fragments

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/FilePanel.js        |  3 +-
 .../structures/NotificationPanel.js           |  3 +-
 src/components/structures/RoomSubList.js      |  2 +-
 .../views/elements/AccessibleTooltipButton.js | 25 +++++-----
 .../views/groups/GroupMemberInfo.js           |  2 +-
 .../views/groups/GroupMemberList.js           |  2 +-
 src/components/views/groups/GroupRoomInfo.js  |  2 +-
 src/components/views/groups/GroupRoomList.js  |  2 +-
 .../views/right_panel/HeaderButton.js         |  5 +-
 .../views/right_panel/HeaderButtons.js        |  2 +-
 src/components/views/rooms/MemberInfo.js      | 50 +++++++++----------
 src/components/views/rooms/MemberList.js      |  2 +-
 src/components/views/rooms/RoomTile.js        |  7 +--
 src/components/views/rooms/Stickerpicker.js   |  4 +-
 .../views/rooms/ThirdPartyMemberInfo.js       |  2 +-
 src/i18n/strings/en_EN.json                   |  6 +--
 16 files changed, 62 insertions(+), 57 deletions(-)

diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js
index 2b9594581e..fb2bdcad42 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.js
@@ -126,11 +126,12 @@ const FilePanel = createReactClass({
                     tileShape="file_grid"
                     resizeNotifier={this.props.resizeNotifier}
                     empty={_t('There are no visible files in this room')}
+                    role="tabpanel"
                 />
             );
         } else {
             return (
-                <div className="mx_FilePanel">
+                <div className="mx_FilePanel" role="tabpanel">
                     <Loader />
                 </div>
             );
diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js
index f9ce0e008e..3a07bf2e63 100644
--- a/src/components/structures/NotificationPanel.js
+++ b/src/components/structures/NotificationPanel.js
@@ -46,12 +46,13 @@ const NotificationPanel = createReactClass({
                     showUrlPreview={false}
                     tileShape="notif"
                     empty={_t('You have no visible notifications')}
+                    role="tabpanel"
                 />
             );
         } else {
             console.error("No notifTimelineSet available!");
             return (
-                <div className="mx_NotificationPanel">
+                <div className="mx_NotificationPanel" role="tabpanel">
                     <Loader />
                 </div>
             );
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index f1057819ff..3d09c05c43 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -258,7 +258,7 @@ const RoomSubList = createReactClass({
         const tabindex = this.props.isFiltered ? "0" : "-1";
         return (
             <div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
-                <AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
+                <AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex} aria-expanded={!isCollapsed}>
                     { chevron }
                     <span>{this.props.label}</span>
                     { incomingCall }
diff --git a/src/components/views/elements/AccessibleTooltipButton.js b/src/components/views/elements/AccessibleTooltipButton.js
index c9a08f6a47..c824ea4025 100644
--- a/src/components/views/elements/AccessibleTooltipButton.js
+++ b/src/components/views/elements/AccessibleTooltipButton.js
@@ -1,18 +1,19 @@
 /*
- Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+Copyright 2019 The Matrix.org Foundation C.I.C.
 
- 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
+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
+    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.
- */
+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';
@@ -55,7 +56,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
             label={title}
         /> : <div />;
         return (
-            <AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
+            <AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
                 { tip }
             </AccessibleButton>
         );
diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js
index 75e647aa4b..3dac90fc35 100644
--- a/src/components/views/groups/GroupMemberInfo.js
+++ b/src/components/views/groups/GroupMemberInfo.js
@@ -183,7 +183,7 @@ module.exports = createReactClass({
 
         const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
         return (
-            <div className="mx_MemberInfo">
+            <div className="mx_MemberInfo" role="tabpanel">
                 <GeminiScrollbarWrapper autoshow={true}>
                     <AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
                         <img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js
index d13f54579d..433625419d 100644
--- a/src/components/views/groups/GroupMemberList.js
+++ b/src/components/views/groups/GroupMemberList.js
@@ -222,7 +222,7 @@ export default createReactClass({
         }
 
         return (
-            <div className="mx_MemberList">
+            <div className="mx_MemberList" role="tabpanel">
                 { inviteButton }
                 <GeminiScrollbarWrapper autoshow={true}>
                     { joined }
diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js
index c6d07cee50..420c163769 100644
--- a/src/components/views/groups/GroupRoomInfo.js
+++ b/src/components/views/groups/GroupRoomInfo.js
@@ -214,7 +214,7 @@ module.exports = createReactClass({
 
         const groupRoomName = this.state.groupRoom.displayname;
         return (
-            <div className="mx_MemberInfo">
+            <div className="mx_MemberInfo" role="tabpanel">
                 <GeminiScrollbarWrapper autoshow={true}>
                     <AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
                         <img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js
index 81921568d0..d57d5e313f 100644
--- a/src/components/views/groups/GroupRoomList.js
+++ b/src/components/views/groups/GroupRoomList.js
@@ -153,7 +153,7 @@ export default createReactClass({
         const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
         const TruncatedList = sdk.getComponent("elements.TruncatedList");
         return (
-            <div className="mx_GroupRoomList">
+            <div className="mx_GroupRoomList" role="tabpanel">
                 { inviteButton }
                 <GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
                     <TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
diff --git a/src/components/views/right_panel/HeaderButton.js b/src/components/views/right_panel/HeaderButton.js
index 2c1e15898e..06b434c1ce 100644
--- a/src/components/views/right_panel/HeaderButton.js
+++ b/src/components/views/right_panel/HeaderButton.js
@@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
 Copyright 2017 Vector Creations Ltd
 Copyright 2017 New Vector Ltd
 Copyright 2018 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -42,8 +43,8 @@ export default class HeaderButton extends React.Component {
         });
 
         return <AccessibleButton
-            aria-label={this.props.title}
-            aria-expanded={this.props.isHighlighted}
+            aria-selected={this.props.isHighlighted}
+            role="tab"
             title={this.props.title}
             className={classes}
             onClick={this.onClick}>
diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js
index 2fa9935ab8..a01b511dc8 100644
--- a/src/components/views/right_panel/HeaderButtons.js
+++ b/src/components/views/right_panel/HeaderButtons.js
@@ -91,7 +91,7 @@ export default class HeaderButtons extends React.Component {
 
     render() {
         // inline style as this will be swapped around in future commits
-        return <div className="mx_HeaderButtons">
+        return <div className="mx_HeaderButtons" role="tablist">
             { this.renderButtons() }
         </div>;
     }
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 1127c53854..2c667b83df 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -1124,35 +1124,35 @@ module.exports = createReactClass({
         }
 
         return (
-            <div className="mx_MemberInfo">
-                    <div className="mx_MemberInfo_name">
-                        { backButton }
-                        { e2eIconElement }
-                        <h2>{ memberName }</h2>
+            <div className="mx_MemberInfo" role="tabpanel">
+                <div className="mx_MemberInfo_name">
+                    { backButton }
+                    { e2eIconElement }
+                    <h2>{ memberName }</h2>
+                </div>
+                { avatarElement }
+                <div className="mx_MemberInfo_container">
+
+                    <div className="mx_MemberInfo_profile">
+                        <div className="mx_MemberInfo_profileField">
+                            { this.props.member.userId }
+                        </div>
+                        { roomMemberDetails }
                     </div>
-                    { avatarElement }
+                </div>
+                <AutoHideScrollbar className="mx_MemberInfo_scrollContainer">
                     <div className="mx_MemberInfo_container">
+                        { this._renderUserOptions() }
 
-                        <div className="mx_MemberInfo_profile">
-                            <div className="mx_MemberInfo_profileField">
-                                { this.props.member.userId }
-                            </div>
-                            { roomMemberDetails }
-                        </div>
+                        { adminTools }
+
+                        { startChat }
+
+                        { this._renderDevices() }
+
+                        { spinner }
                     </div>
-                    <AutoHideScrollbar className="mx_MemberInfo_scrollContainer">
-                        <div className="mx_MemberInfo_container">
-                            { this._renderUserOptions() }
-
-                            { adminTools }
-
-                            { startChat }
-
-                            { this._renderDevices() }
-
-                            { spinner }
-                        </div>
-                    </AutoHideScrollbar>
+                </AutoHideScrollbar>
             </div>
         );
     },
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index 1ecb04d442..0805c0342c 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -475,7 +475,7 @@ module.exports = createReactClass({
         }
 
         return (
-            <div className="mx_MemberList">
+            <div className="mx_MemberList" role="tabpanel">
                 { inviteButton }
                 <AutoHideScrollbar>
                     <div className="mx_MemberList_wrapper">
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js
index a7ba744e47..f2512e06c2 100644
--- a/src/components/views/rooms/RoomTile.js
+++ b/src/components/views/rooms/RoomTile.js
@@ -382,14 +382,15 @@ module.exports = createReactClass({
             />;
         }
 
+        // The following labels are written in such a fashion to increase screen reader efficiency (speed).
         if (notifBadges && mentionBadges && !isInvite) {
-            ariaLabel += " " + _t("It has %(count)s unread messages including mentions.", {
+            ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
                 count: notificationCount,
             });
         } else if (notifBadges) {
-            ariaLabel += " " + _t("It has %(count)s unread messages.", { count: notificationCount });
+            ariaLabel += " " + _t("%(count)s unread messages.", { count: notificationCount });
         } else if (mentionBadges && !isInvite) {
-            ariaLabel += " " + _t("It has unread mentions.");
+            ariaLabel += " " + _t("Unread mentions.");
         }
 
         return <AccessibleButton tabIndex="0"
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index abecb1781d..28e51ed12e 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -409,9 +409,9 @@ export default class Stickerpicker extends React.Component {
                 >
                 </AccessibleButton>;
         }
-        return <div>
+        return <React.Fragment>
             {stickersButton}
             {this.state.showStickers && stickerPicker}
-        </div>;
+        </React.Fragment>;
     }
 }
diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js
index 754e32871f..db6ab479a3 100644
--- a/src/components/views/rooms/ThirdPartyMemberInfo.js
+++ b/src/components/views/rooms/ThirdPartyMemberInfo.js
@@ -121,7 +121,7 @@ export default class ThirdPartyMemberInfo extends React.Component {
 
         // We shamelessly rip off the MemberInfo styles here.
         return (
-            <div className="mx_MemberInfo">
+            <div className="mx_MemberInfo" role="tabpanel">
                 <div className="mx_MemberInfo_name">
                     <AccessibleButton className="mx_MemberInfo_cancel"
                         onClick={this.onCancel}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 43d6bd570d..161681b6b6 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -950,9 +950,9 @@
     "Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
     "Not now": "Not now",
     "Don't ask me again": "Don't ask me again",
-    "It has %(count)s unread messages including mentions.|other": "It has %(count)s unread messages including mentions.",
-    "It has %(count)s unread messages.|other": "It has %(count)s unread messages.",
-    "It has unread mentions.": "It has unread mentions.",
+    "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
+    "%(count)s unread messages.|other": "%(count)s unread messages.",
+    "Unread mentions.": "Unread mentions.",
     "Add a topic": "Add a topic",
     "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.",
     "This room has already been upgraded.": "This room has already been upgraded.",

From a9b4abaf78caba69deb844bb3a71f02974cdd79d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Oct 2019 21:16:18 +0100
Subject: [PATCH 16/19] Fix backspace without slate focus killing everything

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

diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index 5529fb8f32..66210e2f93 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -401,6 +401,12 @@ const LoggedInView = createReactClass({
             const isClickShortcut = ev.target !== document.body &&
                 (ev.key === "Space" || ev.key === "Enter");
 
+            // XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
+            if (ev.key === "Backspace") {
+                ev.stopPropagation();
+                return;
+            }
+
             if (!isClickShortcut && !canElementReceiveInput(ev.target)) {
                 // synchronous dispatch so we focus before key generates input
                 dis.dispatch({action: 'focus_composer'}, true);

From d5f6d781bb358f8f31c7ec930bfb04e374987ecb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Oct 2019 22:13:10 +0100
Subject: [PATCH 17/19] Fix Community Panel preference not taking effect until
 some refreshing

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

diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index fd315d2540..d57aff4696 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -54,6 +54,8 @@ const LeftPanel = createReactClass({
 
         this._settingWatchRef = SettingsStore.watchSetting(
             "breadcrumbs", null, this._onBreadcrumbsChanged);
+        this._settingWatchRef1 = SettingsStore.watchSetting(
+            "TagPanel.enableTagPanel", null, () => this.forceUpdate());
 
         const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs");
         Analytics.setBreadcrumbs(useBreadcrumbs);
@@ -62,6 +64,7 @@ const LeftPanel = createReactClass({
 
     componentWillUnmount: function() {
         SettingsStore.unwatchSetting(this._settingWatchRef);
+        SettingsStore.unwatchSetting(this._settingWatchRef1);
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {

From 9e8234c98fce26c62339641add94e13ee7cb271f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Oct 2019 23:00:08 +0100
Subject: [PATCH 18/19] Handle null from TimelinePanel.getScrollState in
 RoomView _getScrollState

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

diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 13523104ae..27b9d93e50 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -1417,7 +1417,8 @@ module.exports = createReactClass({
 
         const scrollState = messagePanel.getScrollState();
 
-        if (scrollState.stuckAtBottom) {
+        // getScrollState on TimelinePanel *may* return null, so guard against that
+        if (!scrollState || scrollState.stuckAtBottom) {
             // we don't really expect to be in this state, but it will
             // occasionally happen when no scroll state has been set on the
             // messagePanel (ie, we didn't have an initial event (so it's

From 9e9c56d327b3e88c253d21d7727b28b870655b06 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Oct 2019 23:21:32 +0100
Subject: [PATCH 19/19] rename settings watcher refs in LeftPanel

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

diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index d57aff4696..36dd3a7a61 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -52,9 +52,9 @@ const LeftPanel = createReactClass({
     componentWillMount: function() {
         this.focusedElement = null;
 
-        this._settingWatchRef = SettingsStore.watchSetting(
+        this._breadcrumbsWatcherRef = SettingsStore.watchSetting(
             "breadcrumbs", null, this._onBreadcrumbsChanged);
-        this._settingWatchRef1 = SettingsStore.watchSetting(
+        this._tagPanelWatcherRef = SettingsStore.watchSetting(
             "TagPanel.enableTagPanel", null, () => this.forceUpdate());
 
         const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs");
@@ -63,8 +63,8 @@ const LeftPanel = createReactClass({
     },
 
     componentWillUnmount: function() {
-        SettingsStore.unwatchSetting(this._settingWatchRef);
-        SettingsStore.unwatchSetting(this._settingWatchRef1);
+        SettingsStore.unwatchSetting(this._breadcrumbsWatcherRef);
+        SettingsStore.unwatchSetting(this._tagPanelWatcherRef);
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {