From 425f091af753968f7063e3ef659c4747d61d0220 Mon Sep 17 00:00:00 2001
From: Tomasz Knapik <hi@tmkn.org>
Date: Tue, 29 Jan 2019 14:37:11 +0000
Subject: [PATCH] Add rich text editor

---
 .../src/app/src/components/NoteListingItem.js |   2 +-
 .../app/src/components/RichTextForm/index.js  |  42 +++-
 .../src/app/src/containers/AddNoteForm.js     |  30 +--
 .../static_src/src/app/src/containers/Note.js |   4 +-
 .../src/app/src/containers/NoteListing.js     |  14 +-
 .../src/app/src/redux/reducers/submissions.js |   2 +-
 package-lock.json                             | 212 +++++++++++++++++-
 package.json                                  |   2 +
 8 files changed, 268 insertions(+), 40 deletions(-)

diff --git a/opentech/static_src/src/app/src/components/NoteListingItem.js b/opentech/static_src/src/app/src/components/NoteListingItem.js
index c7f88178d..17f89c38b 100644
--- a/opentech/static_src/src/app/src/components/NoteListingItem.js
+++ b/opentech/static_src/src/app/src/components/NoteListingItem.js
@@ -14,7 +14,7 @@ export default class NoteListingItem extends React.Component {
         return (
             <div>
                 <div style={{fontWeight: 'bold'}}>{user} - {timestamp.format('ll')}</div>
-                <div>{message}</div>
+                <div dangerouslySetInnerHTML={{__html: message}} />
             </div>
         );
     }
diff --git a/opentech/static_src/src/app/src/components/RichTextForm/index.js b/opentech/static_src/src/app/src/components/RichTextForm/index.js
index 7934c2748..9724687d5 100644
--- a/opentech/static_src/src/app/src/components/RichTextForm/index.js
+++ b/opentech/static_src/src/app/src/components/RichTextForm/index.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import RichTextEditor from 'react-rte';
 
 export default class RichTextForm extends React.Component {
     static defaultProps = {
@@ -9,24 +10,53 @@ export default class RichTextForm extends React.Component {
 
     static propTypes = {
         disabled: PropTypes.bool.isRequired,
-        initialValue: PropTypes.string,
         onValueChange: PropTypes.func,
         value: PropTypes.string,
+        onSubmit: PropTypes.func,
     };
 
+    state = {
+        value: RichTextEditor.createEmptyValue(),
+    };
+
+    setEditor = editor => {
+        this.editor = editor;
+    }
+
+    resetEditor = () => {
+        this.setState({value: RichTextEditor.createEmptyValue()});
+    }
+
     render() {
         const passProps = {
             disabled: this.props.disabled,
-            defaultValue: this.props.initialValue,
             onChange: this.handleValueChange,
-            value: this.props.value,
+            value: this.state.value,
+            ref: this.setEditor,
         };
+
         return (
-            <textarea {...passProps} />
+            <div>
+                <RichTextEditor {...passProps} />
+                <button
+                    disabled={this.isEmpty() || this.props.disabled}
+                    onClick={this.handleSubmit}
+                >
+                    Submit
+                </button>
+            </div>
         );
     }
 
-    handleValueChange = evt => {
-        this.props.onValueChange(evt.target.value);
+    isEmpty = () => {
+        return !this.state.value;
+    }
+
+    handleValueChange = value => {
+        this.setState({value});
+    }
+
+    handleSubmit = () => {
+        this.props.onSubmit(this.state.value.toString('markdown'), this.resetEditor);
     }
 }
diff --git a/opentech/static_src/src/app/src/containers/AddNoteForm.js b/opentech/static_src/src/app/src/containers/AddNoteForm.js
index 3a92dc695..88c94af36 100644
--- a/opentech/static_src/src/app/src/containers/AddNoteForm.js
+++ b/opentech/static_src/src/app/src/containers/AddNoteForm.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 
 import { createNoteForSubmission } from '@actions/notes';
 import RichTextForm from '@components/RichTextForm';
+
 import {
     getNoteCreatingErrorForSubmission,
     getNoteCreatingStateForSubmission,
@@ -17,10 +18,6 @@ class AddNoteForm extends React.Component {
         isCreating: PropTypes.bool,
     };
 
-    state = {
-        text: '',
-    };
-
     render() {
         const { error, isCreating } = this.props;
         return (
@@ -28,35 +25,22 @@ class AddNoteForm extends React.Component {
                 {Boolean(error) && <p>{error}</p>}
                 <RichTextForm
                     disabled={isCreating}
-                    value={this.state.text}
-                    onValueChange={this.setText} />
-                <button
-                    disabled={!this.state.text.trim() || isCreating}
-                    onClick={this.onSubmit}
-                >
-                    Submit
-                </button>
+                    onSubmit={this.onSubmit}
+                />
             </>
         );
     }
 
-    onSubmit = async () => {
+    onSubmit = async (message, resetEditor) => {
         const action = await this.props.submitNote(this.props.submissionID, {
-            message: this.state.text.trim(),
+            message,
             visibility: 'internal',
         });
+
         if (action === true) {
-            this.setState({
-                text: ''
-            });
+            resetEditor();
         }
     }
-
-    setText = text => {
-        this.setState({
-            text
-        });
-    }
 }
 
 const mapStateToProps = (state, ownProps) => ({
diff --git a/opentech/static_src/src/app/src/containers/Note.js b/opentech/static_src/src/app/src/containers/Note.js
index 9f297145b..c5a94889e 100644
--- a/opentech/static_src/src/app/src/containers/Note.js
+++ b/opentech/static_src/src/app/src/containers/Note.js
@@ -2,6 +2,7 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import moment from 'moment';
+import { markdown } from 'markdown';
 
 import { getNoteOfID } from '@selectors/notes';
 import NoteListingItem from '@components/NoteListingItem';
@@ -17,10 +18,11 @@ class Note extends React.Component {
 
     render() {
         const { note } = this.props;
+        console.log(note.timestamp);
 
         return <NoteListingItem
                 user={note.user}
-                message={note.message}
+                message={markdown.toHTML(note.message)}
                 timestamp={moment(note.timestamp)}
         />;
     }
diff --git a/opentech/static_src/src/app/src/containers/NoteListing.js b/opentech/static_src/src/app/src/containers/NoteListing.js
index 7f16d6c64..c2f2b5c9a 100644
--- a/opentech/static_src/src/app/src/containers/NoteListing.js
+++ b/opentech/static_src/src/app/src/containers/NoteListing.js
@@ -21,14 +21,22 @@ class NoteListing extends React.Component {
     };
 
     componentDidUpdate(prevProps) {
-        const { submissionID } = this.props;
+        const { isLoading, loadNotes, submissionID } = this.props;
         const prevSubmissionID = prevProps.submissionID;
 
         if(
             submissionID !== null && submissionID !== undefined &&
-            prevSubmissionID !== submissionID && !this.props.isLoading
+            prevSubmissionID !== submissionID && !isLoading
         ) {
-            this.props.loadNotes(submissionID);
+            loadNotes(submissionID);
+        }
+    }
+
+    componentDidMount() {
+        const { isLoading, loadNotes, submissionID } = this.props;
+
+        if (submissionID && !isLoading) {
+            loadNotes(submissionID);
         }
     }
 
diff --git a/opentech/static_src/src/app/src/redux/reducers/submissions.js b/opentech/static_src/src/app/src/redux/reducers/submissions.js
index 03abc4b6b..0c5253fd6 100644
--- a/opentech/static_src/src/app/src/redux/reducers/submissions.js
+++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js
@@ -37,7 +37,7 @@ function submission(state, action) {
                 ...state,
                 comments: [
                     action.noteID,
-                    ...state.comments
+                    ...(state.comments || []),
                 ]
             };
         default:
diff --git a/package-lock.json b/package-lock.json
index 36aeb043e..2fb42fde9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1456,6 +1456,11 @@
             "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
             "dev": true
         },
+        "asap": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+            "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
+        },
         "asn1": {
             "version": "0.2.4",
             "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@@ -1604,6 +1609,22 @@
                 "util.promisify": "1.0.0"
             }
         },
+        "babel-runtime": {
+            "version": "6.26.0",
+            "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+            "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+            "requires": {
+                "core-js": "^2.4.0",
+                "regenerator-runtime": "^0.11.0"
+            },
+            "dependencies": {
+                "regenerator-runtime": {
+                    "version": "0.11.1",
+                    "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+                    "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
+                }
+            }
+        },
         "bach": {
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz",
@@ -2166,6 +2187,11 @@
             "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
             "dev": true
         },
+        "class-autobind": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/class-autobind/-/class-autobind-0.1.4.tgz",
+            "integrity": "sha1-NFFsSRZ8+NP2Od3Bhrz6Imiv/zQ="
+        },
         "class-utils": {
             "version": "0.3.6",
             "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -2187,6 +2213,11 @@
                 }
             }
         },
+        "classnames": {
+            "version": "2.2.6",
+            "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
+            "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
+        },
         "clean-css": {
             "version": "4.2.1",
             "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
@@ -3109,6 +3140,63 @@
                 "domelementtype": "1.3.1"
             }
         },
+        "draft-js": {
+            "version": "0.10.5",
+            "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.10.5.tgz",
+            "integrity": "sha512-LE6jSCV9nkPhfVX2ggcRLA4FKs6zWq9ceuO/88BpXdNCS7mjRTgs0NsV6piUCJX9YxMsB9An33wnkMmU2sD2Zg==",
+            "requires": {
+                "fbjs": "^0.8.15",
+                "immutable": "~3.7.4",
+                "object-assign": "^4.1.0"
+            }
+        },
+        "draft-js-export-html": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/draft-js-export-html/-/draft-js-export-html-1.2.0.tgz",
+            "integrity": "sha1-HL4reOH+10/CnHzcv9dUBGjsogk=",
+            "requires": {
+                "draft-js-utils": "^1.2.0"
+            }
+        },
+        "draft-js-export-markdown": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/draft-js-export-markdown/-/draft-js-export-markdown-1.3.0.tgz",
+            "integrity": "sha512-kOiDGQ9KehcbYYcwzlkR+Gja6svEwIgId1gz3EtEVsZ09cxZaV13Qlkydm0J5wPy5Omthvdpj0Iw1B2E4BZRZQ==",
+            "requires": {
+                "draft-js-utils": "^1.2.0"
+            }
+        },
+        "draft-js-import-element": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.2.2.tgz",
+            "integrity": "sha512-atwfQFg5YWsKdBiOIkIYxYh9lOsS5gzzDaqV89GgG1UIb/E1689FI9PsH2OmuJ4DUhHouzBWAAPSa5DerGNnBQ==",
+            "requires": {
+                "draft-js-utils": "^1.2.4",
+                "synthetic-dom": "^1.2.0"
+            }
+        },
+        "draft-js-import-html": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.2.1.tgz",
+            "integrity": "sha512-FP1y9kdmOVDvOxoI4ny+H0g4CVoTQwdW++Zjf+qMsnz07NsYOCLcQ34j7TiwuPfArFAcOjBOc41Mn+qOa1G14w==",
+            "requires": {
+                "draft-js-import-element": "^1.2.1"
+            }
+        },
+        "draft-js-import-markdown": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/draft-js-import-markdown/-/draft-js-import-markdown-1.2.3.tgz",
+            "integrity": "sha512-NPcXwWSsIA+uwASzdJWLQM4y+xW1vTDtDdIDHCHfP76i9cx8zYpH75GW8Ezz8L9SW2qetNcFW056Hj2yxRZ+2g==",
+            "requires": {
+                "draft-js-import-element": "^1.2.1",
+                "synthetic-dom": "^1.2.0"
+            }
+        },
+        "draft-js-utils": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.2.4.tgz",
+            "integrity": "sha512-oy7WL6VCcSJ1WOUVCxkU0t/nGoLs/Kv0+zKalC61WjFTN4I2Lt1I8Oj5m4oUFBxfF7K9+0C0U5ilgvb4F4rovg=="
+        },
         "duplexer": {
             "version": "0.1.1",
             "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@@ -3180,6 +3268,14 @@
             "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
             "dev": true
         },
+        "encoding": {
+            "version": "0.1.12",
+            "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
+            "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+            "requires": {
+                "iconv-lite": "~0.4.13"
+            }
+        },
         "end-of-stream": {
             "version": "1.4.1",
             "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -3921,6 +4017,27 @@
                 "websocket-driver": "0.7.0"
             }
         },
+        "fbjs": {
+            "version": "0.8.17",
+            "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
+            "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
+            "requires": {
+                "core-js": "^1.0.0",
+                "isomorphic-fetch": "^2.1.1",
+                "loose-envify": "^1.0.0",
+                "object-assign": "^4.1.0",
+                "promise": "^7.1.1",
+                "setimmediate": "^1.0.5",
+                "ua-parser-js": "^0.7.18"
+            },
+            "dependencies": {
+                "core-js": {
+                    "version": "1.2.7",
+                    "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
+                    "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
+                }
+            }
+        },
         "figgy-pudding": {
             "version": "3.5.1",
             "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
@@ -5711,7 +5828,6 @@
             "version": "0.4.24",
             "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
             "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-            "dev": true,
             "requires": {
                 "safer-buffer": "2.1.2"
             }
@@ -5749,6 +5865,11 @@
             "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
             "dev": true
         },
+        "immutable": {
+            "version": "3.7.6",
+            "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
+            "integrity": "sha1-E7TTyxK++hVIKib+Gy665kAHHks="
+        },
         "import-fresh": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@@ -6260,8 +6381,7 @@
         "is-stream": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
-            "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
-            "dev": true
+            "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
         },
         "is-symbol": {
             "version": "1.0.2",
@@ -6320,6 +6440,15 @@
             "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
             "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
         },
+        "isomorphic-fetch": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
+            "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
+            "requires": {
+                "node-fetch": "^1.0.1",
+                "whatwg-fetch": ">=0.10.0"
+            }
+        },
         "isstream": {
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -6793,6 +6922,24 @@
                 "object-visit": "1.0.1"
             }
         },
+        "markdown": {
+            "version": "0.5.0",
+            "resolved": "https://registry.npmjs.org/markdown/-/markdown-0.5.0.tgz",
+            "integrity": "sha1-KCBbVlqK51kt4gdGPWY33BgnIrI=",
+            "requires": {
+                "nopt": "~2.1.1"
+            },
+            "dependencies": {
+                "nopt": {
+                    "version": "2.1.2",
+                    "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz",
+                    "integrity": "sha1-bMzZd7gBMqB3MdbozljCyDA8+a8=",
+                    "requires": {
+                        "abbrev": "1"
+                    }
+                }
+            }
+        },
         "matchdep": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
@@ -7249,6 +7396,15 @@
             "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
             "dev": true
         },
+        "node-fetch": {
+            "version": "1.7.3",
+            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
+            "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
+            "requires": {
+                "encoding": "^0.1.11",
+                "is-stream": "^1.0.1"
+            }
+        },
         "node-forge": {
             "version": "0.7.5",
             "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
@@ -8151,6 +8307,14 @@
             "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
             "dev": true
         },
+        "promise": {
+            "version": "7.3.1",
+            "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+            "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+            "requires": {
+                "asap": "~2.0.3"
+            }
+        },
         "promise-inflight": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -8411,6 +8575,30 @@
                 }
             }
         },
+        "react-rte": {
+            "version": "0.16.1",
+            "resolved": "https://registry.npmjs.org/react-rte/-/react-rte-0.16.1.tgz",
+            "integrity": "sha512-CD5kf+6CHqOgJ1yB0i9tkMMch13wOXW5/FQx60gb7nzhXC1ZeFJjtW9dYfCVlfw1AvksHf+lMmKTjhIwyfZR7w==",
+            "requires": {
+                "babel-runtime": "^6.23.0",
+                "class-autobind": "^0.1.4",
+                "classnames": "^2.2.5",
+                "draft-js": ">=0.10.0",
+                "draft-js-export-html": ">=0.6.0",
+                "draft-js-export-markdown": ">=0.3.0",
+                "draft-js-import-html": ">=0.4.0",
+                "draft-js-import-markdown": ">=0.3.0",
+                "draft-js-utils": ">=0.2.0",
+                "immutable": "^3.8.1"
+            },
+            "dependencies": {
+                "immutable": {
+                    "version": "3.8.2",
+                    "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
+                    "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
+                }
+            }
+        },
         "react-transition-group": {
             "version": "2.5.3",
             "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz",
@@ -9435,8 +9623,7 @@
         "setimmediate": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
-            "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
-            "dev": true
+            "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
         },
         "setprototypeof": {
             "version": "1.1.0",
@@ -10077,6 +10264,11 @@
             "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
             "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
         },
+        "synthetic-dom": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.2.0.tgz",
+            "integrity": "sha1-81iar+K14pnzN7sylzqb5C3VYl4="
+        },
         "table": {
             "version": "4.0.3",
             "resolved": "http://registry.npmjs.org/table/-/table-4.0.3.tgz",
@@ -10508,6 +10700,11 @@
             "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
             "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
         },
+        "ua-parser-js": {
+            "version": "0.7.19",
+            "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
+            "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
+        },
         "uglify-js": {
             "version": "3.4.9",
             "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
@@ -11554,6 +11751,11 @@
             "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
             "dev": true
         },
+        "whatwg-fetch": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
+            "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
+        },
         "which": {
             "version": "1.3.1",
             "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
diff --git a/package.json b/package.json
index c9a7457a3..5757f29e3 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
         "gulp-touch-cmd": "0.0.1",
         "gulp-uglify": "^3.0.1",
         "js-cookie": "^2.2.0",
+        "markdown": "^0.5.0",
         "moment": "^2.24.0",
         "moment-timezone": "^0.5.23",
         "node-sass-import-once": "^1.2.0",
@@ -31,6 +32,7 @@
         "react": "^16.7.0",
         "react-dom": "^16.7.0",
         "react-redux": "^6.0.0",
+        "react-rte": "^0.16.1",
         "react-transition-group": "^2.5.3",
         "react-window-size-listener": "^1.2.3",
         "redux": "^4.0.1",
-- 
GitLab