Browse Source

m.text and m.image events

pull/2/head
f0x 4 months ago
parent
commit
b4baae8023
9 changed files with 478 additions and 41 deletions
  1. 2
    2
      app.js
  2. 42
    18
      backends/matrix.js
  3. 27
    0
      components/Event.js
  4. 17
    8
      components/chat.js
  5. 38
    0
      components/events/image.js
  6. 30
    0
      components/events/text.js
  7. 227
    0
      lib/riot-utils.js
  8. 2
    0
      package.json
  9. 93
    13
      shrinkwrap.yaml

+ 2
- 2
app.js View File

@@ -18,7 +18,7 @@ const Input = require('./components/input.js')
18 18
 // incoming/outgoing message alignment (split)
19 19
 
20 20
 
21
-let backend = new Matrix("user", "pass", "http://localhost")
21
+let backend = new Matrix("user", "pass", "https://lain.haus")
22 22
 backend.sync()
23 23
 
24 24
 let App = create({
@@ -48,7 +48,7 @@ let App = create({
48 48
         <Sidebar rooms={this.state.rooms}/>
49 49
         <div className="main">
50 50
           <Info />
51
-          <Chat events={this.state.events}/>
51
+          <Chat events={this.state.events} backend={backend}/>
52 52
           <Input />
53 53
         </div>
54 54
       </>

+ 42
- 18
backends/matrix.js View File

@@ -1,15 +1,45 @@
1 1
 'use strict'
2 2
 const React = require('react')
3 3
 const ReactDOM = require('react-dom')
4
-const defaultValue = require('default-value');
5
-
6
-const components = require('../components/backends/Matrix.js');
4
+const defaultValue = require('default-value')
7 5
 
8 6
 class Matrix {
9 7
   constructor(user, password, homeserver) {
8
+    this.user = user;
9
+    this.password = password;
10
+    this.homeserver = homeserver;
10 11
     this.a = 0
11 12
     this.events = {
12 13
       "roomId": [
14
+        {
15
+          type: "m.room.message",
16
+          sender: "@f0x:lain.haus",
17
+          content: {
18
+            body: "Image caption",
19
+            info: {
20
+              size: 1331429,
21
+              mimetype: "image/png",
22
+              thumbnail_info: {
23
+                w: 600,
24
+                h: 600,
25
+                mimetype: "image/png",
26
+                size: 151911
27
+              },
28
+              w: 2000,
29
+              h: 2000,
30
+              thumbnail_url: "mxc://lain.haus/PnptnVmLprDNICfhCqIIurHZ"
31
+            },
32
+            msgtype: "m.image",
33
+            url: "mxc://lain.haus/MXtCRwxheuSEVsIyHfyUGJNz"
34
+          },
35
+          event_id: "$155317808164309EPnWP:lain.haus",
36
+          origin_server_ts: 1553178081145,
37
+          unsigned: {
38
+            age: 587,
39
+            transaction_id: "m1553178080798.12"
40
+          },
41
+          room_id: "!bghqZrxFTiDyEUzunK:disroot.org"
42
+        }
13 43
       ]
14 44
     }
15 45
 
@@ -35,6 +65,10 @@ class Matrix {
35 65
     this.updates = true
36 66
   }
37 67
 
68
+  getHS() {
69
+    return this.homeserver
70
+  }
71
+
38 72
   getEvents(roomId) {
39 73
     return this.events["roomId"]
40 74
   }
@@ -55,6 +89,10 @@ class Matrix {
55 89
     return false
56 90
   }
57 91
 
92
+  addEvent(event) {
93
+    this.events["roomId"].push(event)
94
+  }
95
+
58 96
   sync() {
59 97
     let rand = this.lastRand
60 98
     while(rand == this.lastRand) {
@@ -80,7 +118,7 @@ class Matrix {
80 118
         age: 1234
81 119
       }
82 120
     }
83
-    this.events["roomId"].push(this.getReactEvent(event))
121
+    this.events["roomId"].push(event)
84 122
     setTimeout(() => {this.sync()}, 2000)
85 123
   }
86 124
 
@@ -93,20 +131,6 @@ class Matrix {
93 131
     }
94 132
     return id
95 133
   }
96
-
97
-  getReactEvent(event) {
98
-    let msgTypes = {
99
-      "m.text": components.text
100
-    }
101
-    if (event.type == "m.room.message") {
102
-      let msgtype = event.content.msgtype
103
-      return React.createElement(
104
-        defaultValue(msgTypes[msgtype], components.text),
105
-        {event: event, key: event.event_id}
106
-      )
107
-    }
108
-  }
109 134
 }
110 135
 
111
-
112 136
 module.exports = Matrix

+ 27
- 0
components/Event.js View File

@@ -0,0 +1,27 @@
1
+'use strict'
2
+const React = require('react')
3
+const ReactDOM = require('react-dom')
4
+const create = require('create-react-class')
5
+const Promise = require('bluebird')
6
+
7
+let Event = create({
8
+  displayName: "Event",
9
+
10
+  render: function() {
11
+    let eventBody = this.props.event.content.split("\n").map((line, id) => {
12
+      if (line.startsWith("image")) {
13
+        return <img key={id} src="neo.png"/>
14
+      }
15
+      return <span key={id}>{line}<br/></span>
16
+    })
17
+
18
+    return <div className="event">
19
+      <div className="body">
20
+        {eventBody}
21
+      </div>
22
+    </div>
23
+  }
24
+})
25
+
26
+
27
+module.exports = Event;

+ 17
- 8
components/chat.js View File

@@ -5,8 +5,12 @@ const create = require('create-react-class')
5 5
 const Promise = require('bluebird')
6 6
 const debounce = require('debounce')
7 7
 const jdenticon = require('jdenticon')
8
+const defaultValue = require('default-value')
8 9
 
9
-const Matrix = require('./backends/Matrix.js')
10
+const elements = {
11
+  "m.text": require('./events/text.js'),
12
+  "m.image": require('./events/image.js')
13
+}
10 14
 
11 15
 jdenticon.config = {
12 16
     lightness: {
@@ -67,7 +71,7 @@ let chat = create({
67 71
     messageGroups.groups.push(messageGroups.current)
68 72
 
69 73
     let events = messageGroups.groups.map((events, id) => {
70
-      return <EventGroup events={events} key={id}/>
74
+      return <EventGroup key={id} events={events} backend={this.props.backend}/>
71 75
     })
72 76
 
73 77
     //TODO: replace with something that only renders events in view
@@ -83,11 +87,10 @@ let EventGroup = create({
83 87
   displayName: "EventGroup",
84 88
 
85 89
   getInitialState: function() {
86
-    console.log(this.props.events);
87 90
     let color = ["red", "green", "yellow", "blue", "purple", "cyan"][Math.floor(Math.random()*6)]
88 91
     return {
89 92
       color: color,
90
-      sender: this.props.events[0].props.event.sender
93
+      sender: this.props.events[0].sender
91 94
     }
92 95
   },
93 96
 
@@ -96,10 +99,9 @@ let EventGroup = create({
96 99
   },
97 100
 
98 101
   render: function() {
99
-    let events = this.props.events;
100
-    //let events = this.props.events.map((event, id) => {
101
-      //return event
102
-    //})
102
+    let events = this.props.events.map((event, id) => {
103
+      return getRenderedEvent(event, id, this.props.backend)
104
+    })
103 105
     return <div className="eventGroup">
104 106
       <svg id="avatar" ref={this.avatarRef} ></svg>
105 107
       <div className="col">
@@ -110,4 +112,11 @@ let EventGroup = create({
110 112
   }
111 113
 })
112 114
 
115
+function getRenderedEvent(event, id, backend) {
116
+  if (event.type == "m.room.message") {
117
+    let msgtype = event.content.msgtype;
118
+    return React.createElement(elements[defaultValue(msgtype, "m.text")], {event: event, key: id, backend: backend})
119
+  }
120
+}
121
+
113 122
 module.exports = chat

+ 38
- 0
components/events/image.js View File

@@ -0,0 +1,38 @@
1
+'use strict'
2
+const React = require('react')
3
+const ReactDOM = require('react-dom')
4
+const create = require('create-react-class')
5
+const Promise = require('bluebird')
6
+
7
+const Text = require('./text.js')
8
+
9
+let Event = create({
10
+  displayName: "m.image",
11
+
12
+  getInitialState: function() {
13
+    let hs = this.props.backend.getHS()
14
+    let event = this.props.event;
15
+    let media_mxc = event.content.url.slice(7);
16
+    let thumb_mxc = event.content.info.thumbnail_url.slice(6);
17
+    let base = `${hs}/_matrix/media/v1/download`
18
+    return {
19
+      url: {
20
+        media: `${base}/${media_mxc}`,
21
+        thumb: `${base}/${thumb_mxc}`
22
+      }
23
+    }
24
+  },
25
+
26
+  render: function() {
27
+    return <div className="event">
28
+      <div className="body">
29
+        <a href={this.state.url.media} target="_blank">
30
+          <img src={this.state.url.thumb}/>
31
+        </a>
32
+        <Text event={this.props.event} nested={true}/>
33
+      </div>
34
+    </div>
35
+  }
36
+})
37
+
38
+module.exports = Event;

+ 30
- 0
components/events/text.js View File

@@ -0,0 +1,30 @@
1
+'use strict'
2
+const React = require('react')
3
+const ReactDOM = require('react-dom')
4
+const create = require('create-react-class')
5
+const Promise = require('bluebird')
6
+
7
+const riot = require('../../lib/riot-utils.js')
8
+
9
+let Event = create({
10
+  displayName: "m.text",
11
+
12
+  render: function() {
13
+    let event = this.props.event;
14
+    // let eventBody = event.content.body.split("\n").map((line, id) => {
15
+    //   return <span key={id}>{line}<br/></span>
16
+    // })
17
+
18
+    let eventBody = riot.sanitize(event.content.body)
19
+
20
+    return <div className="event">
21
+      <div
22
+        className={this.props.nested ? "nested" : "body"}
23
+        dangerouslySetInnerHTML={{__html: eventBody}}
24
+      />
25
+    </div>
26
+  }
27
+})
28
+
29
+
30
+module.exports = Event;

+ 227
- 0
lib/riot-utils.js View File

@@ -0,0 +1,227 @@
1
+'use strict';
2
+/*
3
+Copyright 2015, 2016 OpenMarket Ltd
4
+Licensed under the Apache License, Version 2.0 (the "License");
5
+you may not use this file except in compliance with the License.
6
+You may obtain a copy of the License at
7
+    http://www.apache.org/licenses/LICENSE-2.0
8
+Unless required by applicable law or agreed to in writing, software
9
+distributed under the License is distributed on an "AS IS" BASIS,
10
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+See the License for the specific language governing permissions and
12
+limitations under the License.
13
+*/
14
+
15
+const Promise = require('bluebird');
16
+const sanitize = require('sanitize-html');
17
+require("blueimp-canvas-to-blob");
18
+const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
19
+
20
+/**
21
+ * Create a thumbnail for a image DOM element.
22
+ * The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
23
+ * The thumbnail will have the same aspect ratio as the original.
24
+ * Draws the element into a canvas using CanvasRenderingContext2D.drawImage
25
+ * Then calls Canvas.toBlob to get a blob object for the image data.
26
+ *
27
+ * Since it needs to calculate the dimensions of the source image and the
28
+ * thumbnailed image it returns an info object filled out with information
29
+ * about the original image and the thumbnail.
30
+ *
31
+ * @param {HTMLElement} element The element to thumbnail.
32
+ * @param {integer} inputWidth The width of the image in the input element.
33
+ * @param {integer} inputHeight the width of the image in the input element.
34
+ * @param {String} mimeType The mimeType to save the blob as.
35
+ * @return {Promise} A promise that resolves with an object with an info key
36
+ *  and a thumbnail key.
37
+ */
38
+
39
+module.exports = {
40
+  createThumbnail: function(element, inputWidth, inputHeight, mimeType) {
41
+    return new Promise(function(resolve, reject) {
42
+      const MAX_WIDTH = 800;
43
+      const MAX_HEIGHT = 600;
44
+  
45
+      let targetWidth = inputWidth;
46
+      let targetHeight = inputHeight;
47
+      if (targetHeight > MAX_HEIGHT) {
48
+        targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
49
+        targetHeight = MAX_HEIGHT;
50
+      }
51
+      if (targetWidth > MAX_WIDTH) {
52
+        targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
53
+        targetWidth = MAX_WIDTH;
54
+      }
55
+  
56
+      const canvas = document.createElement("canvas");
57
+      canvas.width = targetWidth;
58
+      canvas.height = targetHeight;
59
+      canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
60
+      canvas.toBlob(function(thumbnail) {
61
+        resolve({
62
+          info: {
63
+            thumbnail_info: {
64
+              w: targetWidth,
65
+              h: targetHeight,
66
+              mimetype: thumbnail.type,
67
+              size: thumbnail.size,
68
+            },
69
+            w: inputWidth,
70
+            h: inputHeight,
71
+          },
72
+          thumbnail: thumbnail,
73
+        });
74
+      }, mimeType);
75
+    });
76
+  },
77
+  
78
+  /**
79
+   * Load a file into a newly created image element.
80
+   *
81
+   * @param {File} file The file to load in an image element.
82
+   * @return {Promise} A promise that resolves with the html image element.
83
+   */
84
+  loadImageElement: function(imageFile) {
85
+    return new Promise(function(resolve, reject) {
86
+      // Load the file into an html element
87
+      const img = document.createElement("img");
88
+      const objectUrl = URL.createObjectURL(imageFile);
89
+      img.src = objectUrl;
90
+  
91
+      // Once ready, create a thumbnail
92
+      img.onload = function() {
93
+        URL.revokeObjectURL(objectUrl);
94
+        resolve(img);
95
+      };
96
+      img.onerror = function(e) {
97
+        reject(e);
98
+      };
99
+    });
100
+  },
101
+
102
+  /**
103
+   * Load a file into a newly created video element.
104
+   *
105
+   * @param {File} file The file to load in an video element.
106
+   * @return {Promise} A promise that resolves with the video image element.
107
+   */
108
+  loadVideoElement: function(videoFile) {
109
+    return new Promise(function(resolve, reject) {
110
+      // Load the file into an html element
111
+      const video = document.createElement("video");
112
+  
113
+      const reader = new FileReader();
114
+      reader.onload = function(e) {
115
+        video.src = e.target.result;
116
+  
117
+        // Once ready, returns its size
118
+        // Wait until we have enough data to thumbnail the first frame.
119
+        video.onloadeddata = function() {
120
+          resolve(video);
121
+        };
122
+        video.onerror = function(e) {
123
+          reject(e);
124
+        };
125
+      };
126
+      reader.onerror = function(e) {
127
+        reject(e);
128
+      };
129
+      reader.readAsDataURL(videoFile);
130
+    });
131
+  },
132
+
133
+  sanitize: function(html) {
134
+    return sanitize(html, this.sanitizeHtmlParams);
135
+  },
136
+
137
+  sanitizeHtmlParams: {
138
+    allowedTags: [
139
+      'font', // custom to matrix for IRC-style font coloring
140
+      'del', // for markdown
141
+      'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a', 'ul', 'ol', 'sup', 'sub',
142
+      'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
143
+      'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
144
+      'mx-reply', 'mx-rainbow'
145
+    ],
146
+    allowedAttributes: {
147
+      // custom ones first:
148
+      font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
149
+      span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
150
+      a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
151
+      img: ['src', 'width', 'height', 'alt', 'title'],
152
+      ol: ['start'],
153
+      code: ['class'], // We don't actually allow all classes, we filter them in transformTags
154
+    },
155
+    // Lots of these won't come up by default because we don't allow them
156
+    selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
157
+    // URL schemes we permit
158
+    allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
159
+
160
+    allowProtocolRelative: false,
161
+
162
+    transformTags: { // custom to matrix
163
+    // add blank targets to all hyperlinks except vector URLs
164
+      'img': function(tagName, attribs) {
165
+        // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
166
+        // because transformTags is used _before_ we filter by allowedSchemesByTag and
167
+        // we don't want to allow images with `https?` `src`s.
168
+        //if (!attribs.src || !attribs.src.startsWith('mxc://')) {
169
+        return { tagName, attribs: {}};
170
+        //}
171
+        //attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
172
+        //  attribs.src,
173
+        //  attribs.width || 800,
174
+        //  attribs.height || 600
175
+        //);
176
+        //return { tagName: tagName, attribs: attribs };
177
+      },
178
+
179
+      'code': function(tagName, attribs) {
180
+        if (typeof attribs.class !== 'undefined') {
181
+          // Filter out all classes other than ones starting with language- for syntax highlighting.
182
+          const classes = attribs.class.split(/\s+/).filter(function(cl) {
183
+            return cl.startsWith('language-');
184
+          });
185
+          attribs.class = classes.join(' ');
186
+        }
187
+        return {
188
+          tagName: tagName,
189
+          attribs: attribs,
190
+        };
191
+      },
192
+
193
+      '*': function(tagName, attribs) {
194
+        // Delete any style previously assigned, style is an allowedTag for font and span
195
+        // because attributes are stripped after transforming
196
+        delete attribs.style;
197
+
198
+        // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
199
+        // equivalents
200
+        const customCSSMapper = {
201
+          'data-mx-color': 'color',
202
+          'data-mx-bg-color': 'background-color',
203
+          // $customAttributeKey: $cssAttributeKey
204
+        };
205
+
206
+        let style = "";
207
+        Object.keys(customCSSMapper).forEach((customAttributeKey) => {
208
+          const cssAttributeKey = customCSSMapper[customAttributeKey];
209
+          const customAttributeValue = attribs[customAttributeKey];
210
+          if (customAttributeValue &&
211
+            typeof customAttributeValue === 'string' &&
212
+            COLOR_REGEX.test(customAttributeValue)
213
+          ) {
214
+            style += cssAttributeKey + ":" + customAttributeValue + ";";
215
+            delete attribs[customAttributeKey];
216
+          }
217
+        });
218
+
219
+        if (style) {
220
+          attribs.style = style;
221
+        }
222
+
223
+        return { tagName: tagName, attribs: attribs };
224
+      },
225
+    },
226
+  }
227
+};

+ 2
- 0
package.json View File

@@ -14,6 +14,7 @@
14 14
     "@babel/preset-react": "^7.0.0",
15 15
     "babelify": "^10.0.0",
16 16
     "bluebird": "^3.5.3",
17
+    "blueimp-canvas-to-blob": "^3.14.0",
17 18
     "browserify": "^16.2.3",
18 19
     "budo": "^11.5.0",
19 20
     "create-react-class": "^15.6.3",
@@ -36,6 +37,7 @@
36 37
     "livereactload": "^4.0.0-beta.2",
37 38
     "react": "^16.6.3",
38 39
     "react-dom": "^16.6.3",
40
+    "sanitize-html": "^1.20.0",
39 41
     "vinyl-buffer": "^1.0.1",
40 42
     "vinyl-source-stream": "^2.0.0",
41 43
     "webpack": "^4.27.1"

+ 93
- 13
shrinkwrap.yaml View File

@@ -4,6 +4,7 @@ dependencies:
4 4
   '@babel/preset-react': 7.0.0
5 5
   babelify: 10.0.0
6 6
   bluebird: 3.5.3
7
+  blueimp-canvas-to-blob: 3.14.0
7 8
   browserify: 16.2.3
8 9
   budo: 11.5.0
9 10
   create-react-class: 15.6.3
@@ -26,6 +27,7 @@ dependencies:
26 27
   livereactload: 4.0.0-beta.2
27 28
   react: 16.6.3
28 29
   react-dom: 16.6.3
30
+  sanitize-html: 1.20.0
29 31
   vinyl-buffer: 1.0.1
30 32
   vinyl-source-stream: 2.0.0
31 33
   webpack: 4.27.1
@@ -1540,6 +1542,10 @@ packages:
1540 1542
     dev: false
1541 1543
     resolution:
1542 1544
       integrity: sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
1545
+  /blueimp-canvas-to-blob/3.14.0:
1546
+    dev: false
1547
+    resolution:
1548
+      integrity: sha512-i6I2CiX1VR8YwUNYBo+dM8tg89ns4TTHxSpWjaDeHKcYS3yFalpLCwDaY21/EsJMufLy2tnG4j0JN5L8OVNkKQ==
1543 1549
   /bn.js/4.11.8:
1544 1550
     dev: false
1545 1551
     resolution:
@@ -2948,14 +2954,13 @@ packages:
2948 2954
       node: '>=4'
2949 2955
     resolution:
2950 2956
       integrity: sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==
2951
-  /dom-serializer/0.1.0:
2957
+  /dom-serializer/0.1.1:
2952 2958
     dependencies:
2953
-      domelementtype: 1.1.3
2959
+      domelementtype: 1.3.1
2954 2960
       entities: 1.1.2
2955 2961
     dev: false
2956
-    optional: true
2957 2962
     resolution:
2958
-      integrity: sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=
2963
+      integrity: sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
2959 2964
   /domain-browser/1.1.7:
2960 2965
     dev: false
2961 2966
     engines:
@@ -2970,22 +2975,21 @@ packages:
2970 2975
       npm: '>=1.2'
2971 2976
     resolution:
2972 2977
       integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
2973
-  /domelementtype/1.1.3:
2974
-    dev: false
2975
-    optional: true
2976
-    resolution:
2977
-      integrity: sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=
2978 2978
   /domelementtype/1.3.1:
2979 2979
     dev: false
2980
-    optional: true
2981 2980
     resolution:
2982 2981
       integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
2982
+  /domhandler/2.4.2:
2983
+    dependencies:
2984
+      domelementtype: 1.3.1
2985
+    dev: false
2986
+    resolution:
2987
+      integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
2983 2988
   /domutils/1.7.0:
2984 2989
     dependencies:
2985
-      dom-serializer: 0.1.0
2990
+      dom-serializer: 0.1.1
2986 2991
       domelementtype: 1.3.1
2987 2992
     dev: false
2988
-    optional: true
2989 2993
     resolution:
2990 2994
       integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
2991 2995
   /download/6.2.5:
@@ -3127,7 +3131,6 @@ packages:
3127 3131
       integrity: sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==
3128 3132
   /entities/1.1.2:
3129 3133
     dev: false
3130
-    optional: true
3131 3134
     resolution:
3132 3135
       integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
3133 3136
   /errno/0.1.7:
@@ -4634,6 +4637,17 @@ packages:
4634 4637
       node: '>=0.10'
4635 4638
     resolution:
4636 4639
       integrity: sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=
4640
+  /htmlparser2/3.10.1:
4641
+    dependencies:
4642
+      domelementtype: 1.3.1
4643
+      domhandler: 2.4.2
4644
+      domutils: 1.7.0
4645
+      entities: 1.1.2
4646
+      inherits: 2.0.3
4647
+      readable-stream: 3.2.0
4648
+    dev: false
4649
+    resolution:
4650
+      integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
4637 4651
   /http-browserify/1.3.2:
4638 4652
     dependencies:
4639 4653
       Base64: 0.2.1
@@ -5700,6 +5714,10 @@ packages:
5700 5714
     dev: false
5701 5715
     resolution:
5702 5716
       integrity: sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=
5717
+  /lodash.escaperegexp/4.1.2:
5718
+    dev: false
5719
+    resolution:
5720
+      integrity: sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
5703 5721
   /lodash.isarguments/3.1.0:
5704 5722
     dev: false
5705 5723
     resolution:
@@ -5714,6 +5732,14 @@ packages:
5714 5732
     dev: false
5715 5733
     resolution:
5716 5734
       integrity: sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=
5735
+  /lodash.isplainobject/4.0.6:
5736
+    dev: false
5737
+    resolution:
5738
+      integrity: sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
5739
+  /lodash.isstring/4.0.1:
5740
+    dev: false
5741
+    resolution:
5742
+      integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
5717 5743
   /lodash.keys/2.4.1:
5718 5744
     dependencies:
5719 5745
       lodash._isnative: 2.4.1
@@ -7100,6 +7126,16 @@ packages:
7100 7126
       node: '>=0.10.0'
7101 7127
     resolution:
7102 7128
       integrity: sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
7129
+  /postcss/7.0.14:
7130
+    dependencies:
7131
+      chalk: 2.4.2
7132
+      source-map: 0.6.1
7133
+      supports-color: 6.1.0
7134
+    dev: false
7135
+    engines:
7136
+      node: '>=6.0.0'
7137
+    resolution:
7138
+      integrity: sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==
7103 7139
   /prepend-http/1.0.4:
7104 7140
     dev: false
7105 7141
     engines:
@@ -7437,6 +7473,16 @@ packages:
7437 7473
       node: '>= 6'
7438 7474
     resolution:
7439 7475
       integrity: sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==
7476
+  /readable-stream/3.2.0:
7477
+    dependencies:
7478
+      inherits: 2.0.3
7479
+      string_decoder: 1.2.0
7480
+      util-deprecate: 1.0.2
7481
+    dev: false
7482
+    engines:
7483
+      node: '>= 6'
7484
+    resolution:
7485
+      integrity: sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==
7440 7486
   /readdirp/2.2.1:
7441 7487
     dependencies:
7442 7488
       graceful-fs: 4.1.15
@@ -7749,6 +7795,21 @@ packages:
7749 7795
     dev: false
7750 7796
     resolution:
7751 7797
       integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
7798
+  /sanitize-html/1.20.0:
7799
+    dependencies:
7800
+      chalk: 2.4.2
7801
+      htmlparser2: 3.10.1
7802
+      lodash.clonedeep: 4.5.0
7803
+      lodash.escaperegexp: 4.1.2
7804
+      lodash.isplainobject: 4.0.6
7805
+      lodash.isstring: 4.0.1
7806
+      lodash.mergewith: 4.6.1
7807
+      postcss: 7.0.14
7808
+      srcset: 1.0.0
7809
+      xtend: 4.0.1
7810
+    dev: false
7811
+    resolution:
7812
+      integrity: sha512-BpxXkBoAG+uKCHjoXFmox6kCSYpnulABoGcZ/R3QyY9ndXbIM5S94eOr1IqnzTG8TnbmXaxWoDDzKC5eJv7fEQ==
7752 7813
   /sass-graph/2.2.4:
7753 7814
     dependencies:
7754 7815
       glob: 7.1.3
@@ -8158,6 +8219,15 @@ packages:
8158 8219
     optional: true
8159 8220
     resolution:
8160 8221
       integrity: sha1-MwRQN7ZDiLVnZ0uEMiplIQc5FsM=
8222
+  /srcset/1.0.0:
8223
+    dependencies:
8224
+      array-uniq: 1.0.3
8225
+      number-is-nan: 1.0.1
8226
+    dev: false
8227
+    engines:
8228
+      node: '>=0.10.0'
8229
+    resolution:
8230
+      integrity: sha1-pWad4StC87HV6D7QPHEEb8SPQe8=
8161 8231
   /sshpk/1.16.0:
8162 8232
     dependencies:
8163 8233
       asn1: 0.2.4
@@ -8448,6 +8518,14 @@ packages:
8448 8518
       node: '>=4'
8449 8519
     resolution:
8450 8520
       integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
8521
+  /supports-color/6.1.0:
8522
+    dependencies:
8523
+      has-flag: 3.0.0
8524
+    dev: false
8525
+    engines:
8526
+      node: '>=6'
8527
+    resolution:
8528
+      integrity: sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
8451 8529
   /sver-compat/1.5.0:
8452 8530
     dependencies:
8453 8531
       es6-iterator: 2.0.3
@@ -9446,6 +9524,7 @@ specifiers:
9446 9524
   '@babel/preset-react': ^7.0.0
9447 9525
   babelify: ^10.0.0
9448 9526
   bluebird: ^3.5.3
9527
+  blueimp-canvas-to-blob: ^3.14.0
9449 9528
   browserify: ^16.2.3
9450 9529
   budo: ^11.5.0
9451 9530
   create-react-class: ^15.6.3
@@ -9469,6 +9548,7 @@ specifiers:
9469 9548
   livereactload: ^4.0.0-beta.2
9470 9549
   react: ^16.6.3
9471 9550
   react-dom: ^16.6.3
9551
+  sanitize-html: ^1.20.0
9472 9552
   vinyl-buffer: ^1.0.1
9473 9553
   vinyl-source-stream: ^2.0.0
9474 9554
   webpack: ^4.27.1

Loading…
Cancel
Save