From e86d2b616e647f85bbfa56cb79bc952ca932ad40 Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Wed, 20 Nov 2019 16:18:28 +0100
Subject: [PATCH] add ToastContainer

---
 res/css/_components.scss                    |   1 +
 res/css/structures/_ToastContainer.scss     | 105 ++++++++++++++++++++
 src/components/structures/LoggedInView.js   |   2 +
 src/components/structures/ToastContainer.js |  85 ++++++++++++++++
 4 files changed, 193 insertions(+)
 create mode 100644 res/css/structures/_ToastContainer.scss
 create mode 100644 src/components/structures/ToastContainer.js

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 40a2c576d0..f7147b3b9f 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -25,6 +25,7 @@
 @import "./structures/_TabbedView.scss";
 @import "./structures/_TagPanel.scss";
 @import "./structures/_TagPanelButtons.scss";
+@import "./structures/_ToastContainer.scss";
 @import "./structures/_TopLeftMenuButton.scss";
 @import "./structures/_UploadBar.scss";
 @import "./structures/_ViewSource.scss";
diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss
new file mode 100644
index 0000000000..54132d19bf
--- /dev/null
+++ b/res/css/structures/_ToastContainer.scss
@@ -0,0 +1,105 @@
+/*
+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_ToastContainer {
+    position: absolute;
+    top: 0;
+    left: 70px;
+    z-index: 101;
+    padding: 4px;
+    display: grid;
+    grid-template-rows: 1fr 14px 6px;
+
+    &.mx_ToastContainer_stacked::before {
+        content: "";
+        margin: 0 4px;
+        grid-row: 2 / 4;
+        grid-column: 1;
+        background-color: white;
+        box-shadow: 0px 4px 12px $menu-box-shadow-color;
+        border-radius: 8px;
+    }
+
+    .mx_Toast_toast {
+        grid-row: 1 / 3;
+        grid-column: 1;
+        color: $primary-fg-color;
+        background-color: $primary-bg-color;
+        box-shadow: 0px 4px 12px $menu-box-shadow-color;
+        border-radius: 8px;
+        overflow: hidden;
+    }
+
+    .mx_Toast_toast {
+        display: grid;
+        grid-template-columns: 20px 1fr;
+        column-gap: 10px;
+        row-gap: 4px;
+        padding: 8px;
+        padding-right: 16px;
+
+        &.mx_Toast_hasIcon {
+            &::after {
+                content: "";
+                width: 20px;
+                height: 20px;
+                grid-column: 1;
+                grid-row: 1;
+                mask-size: 100%;
+                mask-repeat: no-repeat;
+            }
+
+            &.mx_Toast_icon_verification::after {
+                mask-image: url("$(res)/img/e2e/normal.svg");
+                background-color: $primary-fg-color;
+            }
+
+            h2, .mx_Toast_body {
+                grid-column: 2;
+            }
+        }
+
+        h2 {
+            grid-column: 1 / 3;
+            grid-row: 1;
+            margin: 0;
+            font-size: 15px;
+            font-weight: 600;
+        }
+
+        .mx_Toast_body {
+            grid-column: 1 / 3;
+            grid-row: 2;
+        }
+
+        .mx_Toast_buttons {
+            display: flex;
+
+            > :not(:last-child) {
+                margin-right: 8px;
+            }
+        }
+
+        .mx_Toast_description {
+            max-width: 400px;
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            margin: 4px 0 11px 0;
+            font-size: 12px;
+        }
+    }
+}
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index 889b0cdc8b..d071ba1d79 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -525,6 +525,7 @@ const LoggedInView = createReactClass({
         const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
         const GroupView = sdk.getComponent('structures.GroupView');
         const MyGroups = sdk.getComponent('structures.MyGroups');
+        const ToastContainer = sdk.getComponent('structures.ToastContainer');
         const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
         const CookieBar = sdk.getComponent('globals.CookieBar');
         const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
@@ -628,6 +629,7 @@ const LoggedInView = createReactClass({
         return (
             <div onPaste={this._onPaste} onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
                 { topBar }
+                <ToastContainer />
                 <DragDropContext onDragEnd={this._onDragEnd}>
                     <div ref={this._setResizeContainerRef} className={bodyClasses}>
                         <LeftPanel
diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.js
new file mode 100644
index 0000000000..b8ced1e9de
--- /dev/null
+++ b/src/components/structures/ToastContainer.js
@@ -0,0 +1,85 @@
+/*
+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 * as React from "react";
+import dis from "../../dispatcher";
+import { _t } from '../../languageHandler';
+import classNames from "classnames";
+
+export default class ToastContainer extends React.Component {
+    constructor() {
+        super();
+        this.state = {toasts: []};
+    }
+
+    componentDidMount() {
+        this._dispatcherRef = dis.register(this.onAction);
+    }
+
+    componentWillUnmount() {
+        dis.unregister(this._dispatcherRef);
+    }
+
+    onAction = (payload) => {
+        if (payload.action === "show_toast") {
+            this._addToast(payload.toast);
+        }
+    };
+
+    _addToast(toast) {
+        this.setState({toasts: this.state.toasts.concat(toast)});
+    }
+
+    dismissTopToast = () => {
+        const [, ...remaining] = this.state.toasts;
+        this.setState({toasts: remaining});
+    };
+
+    render() {
+        const totalCount = this.state.toasts.length;
+        if (totalCount === 0) {
+            return null;
+        }
+        const isStacked = totalCount > 1;
+        const topToast = this.state.toasts[0];
+        const {title, icon, key, component, props} = topToast;
+
+        const containerClasses = classNames("mx_ToastContainer", {
+            "mx_ToastContainer_stacked": isStacked,
+        });
+
+        const toastClasses = classNames("mx_Toast_toast", {
+            "mx_Toast_hasIcon": icon,
+            [`mx_Toast_icon_${icon}`]: icon,
+        });
+
+        const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
+
+        const toastProps = Object.assign({}, props, {
+            dismiss: this.dismissTopToast,
+            key,
+        });
+
+        return (
+            <div className={containerClasses}>
+                <div className={toastClasses}>
+                    <h2>{title}{countIndicator}</h2>
+                    <div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
+                </div>
+            </div>
+        );
+    }
+}