Add image editor

This commit is contained in:
Josh Perez 2021-11-30 21:14:25 -05:00 committed by GitHub
parent 86d09917a3
commit 7affe313f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 4261 additions and 173 deletions

View File

@ -45,6 +45,30 @@ Signal Desktop makes use of the following open source projects.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## @types/fabric
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
## abort-controller
MIT License
@ -925,6 +949,25 @@ Signal Desktop makes use of the following open source projects.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## fabric
Copyright (c) 2008-2015 Printio (Juriy Zaytsev, Maxim Chernyak)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## fast-glob
The MIT License (MIT)

View File

@ -6566,6 +6566,90 @@
"message": "There was an error when saving your settings. Please try again.",
"description": "Shown if there is an error when saving your preferred reaction settings. Should be very rare to see this message."
},
"MediaEditor__control--draw": {
"message": "Draw",
"description": "Label for the draw button in the media editor"
},
"MediaEditor__control--text": {
"message": "Add text",
"description": "Label for the text button in the media editor"
},
"MediaEditor__control--sticker": {
"message": "Stickers",
"description": "Label for the sticker button in the media editor"
},
"MediaEditor__control--crop": {
"message": "Crop and rotate",
"description": "Label for the crop & rotate button in the media editor"
},
"MediaEditor__control--undo": {
"message": "Undo",
"description": "Label for the undo button in the media editor"
},
"MediaEditor__control--redo": {
"message": "Redo",
"description": "Label for the redo button in the media editor"
},
"MediaEditor__text--regular": {
"message": "Regular",
"description": "Describes what attribute the color picker will change on the text"
},
"MediaEditor__text--highlight": {
"message": "Highlight",
"description": "Describes what attribute the color picker will change on the text"
},
"MediaEditor__text--outline": {
"message": "Outline",
"description": "Describes what attribute the color picker will change on the text"
},
"MediaEditor__text--underline": {
"message": "Underline",
"description": "Describes what attribute the color picker will change on the text"
},
"MediaEditor__draw--pen": {
"message": "Pen",
"description": "Type of brush to free draw"
},
"MediaEditor__draw--highlighter": {
"message": "Highlighter",
"description": "Type of brush to free draw"
},
"MediaEditor__draw--thin": {
"message": "Thin",
"description": "Tip width of the brush"
},
"MediaEditor__draw--regular": {
"message": "Regular",
"description": "Tip width of the brush"
},
"MediaEditor__draw--medium": {
"message": "Medium",
"description": "Tip width of the brush"
},
"MediaEditor__draw--heavy": {
"message": "Heavy",
"description": "Tip width of the brush"
},
"MediaEditor__crop--reset": {
"message": "Reset",
"description": "Reset the crop state"
},
"MediaEditor__crop--rotate": {
"message": "Rotate",
"description": "Rotate the canvas"
},
"MediaEditor__crop--flip": {
"message": "Flip",
"description": "Flip/mirror the canvas"
},
"MediaEditor__crop--lock": {
"message": "Lock",
"description": "Lock the aspect ratio"
},
"MediaEditor__crop--crop": {
"message": "Crop",
"description": "Performs the crop"
},
"WhatsNew__modal-title": {
"message": "What's New",
"description": "Title for the whats new modal"

BIN
fixtures/snow.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2.786,9.824H13.9a.85.85,0,0,1,.84.84v5a.88.88,0,0,0,1.75,0v-5a2.59,2.59,0,0,0-2.59-2.59H6.836v-2.3a.88.88,0,0,0-1.75,0v2.3h-2.3a.88.88,0,0,0,0,1.75Z"/><path d="M19.633,18.93a.9.9,0,0,0-.007-.679.874.874,0,0,0-.494-.473.869.869,0,0,0-.346-.054H7.676a.85.85,0,0,1-.84-.84v-4.93a.88.88,0,0,0-1.75,0v4.93a2.591,2.591,0,0,0,2.59,2.59h7.11v2.3a.88.88,0,0,0,1.7,0v-2.3h2.34a.873.873,0,0,0,.807-.544Z"/><path d="M21.5,9.758a.88.88,0,0,0,.037-.364,6.758,6.758,0,0,0-6.63-6.76v-.26a.86.86,0,0,0-.114-.435.87.87,0,0,0-1.186-.325l-2,1.13a.88.88,0,0,0-.319,1.2.887.887,0,0,0,.319.322l2,1.13a.922.922,0,0,0,.43.11.871.871,0,0,0,.87-.87v-.24a5,5,0,0,1,4.88,5,.881.881,0,0,0,1.234.9.9.9,0,0,0,.3-.215A.888.888,0,0,0,21.5,9.758Z"/></svg>

After

Width:  |  Height:  |  Size: 790 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m15 7.6a1.4 1.4 0 0 1 .2.6v1.3h1.9v-1.3a1.9 1.9 0 0 0 -.3-1 4.1 4.1 0 0 0 -.5-.9l-.9-.5a1.9 1.9 0 0 0 -1-.3h-7.3v-2.3a1.6 1.6 0 0 0 -.3-.6.7.7 0 0 0 -.6-.2c-.3 0-.5 0-.6.2a.6.6 0 0 0 -.3.6v2.3h-2.4l-.6.3a.8.8 0 0 0 -.2.7.7.7 0 0 0 .2.6l.6.3h11.5z"/><path d="m11.2 15.5h-3.3l-.6-.2a1.4 1.4 0 0 1 -.2-.6v-5.1a1.6 1.6 0 0 0 -.3-.6.7.7 0 0 0 -.6-.2c-.3 0-.5 0-.6.2a.6.6 0 0 0 -.3.6v5.1a2.6 2.6 0 0 0 .7 1.9 2.6 2.6 0 0 0 1.9.7h3.3z"/><path d="m21.1 15.9h-.5v-2.6a2.4 2.4 0 0 0 -2.4-2.4 2.4 2.4 0 0 0 -2.4 2.4v2.6h-.5a.7.7 0 0 0 -.8.8v4.1a.7.7 0 0 0 .8.8h5.8a.7.7 0 0 0 .8-.8v-4.1a.7.7 0 0 0 -.8-.8zm-2.9-3.8a1.2 1.2 0 0 1 1.2 1.2v2.6h-2.4v-2.6a1.2 1.2 0 0 1 1.2-1.2z"/></svg>

After

Width:  |  Height:  |  Size: 740 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g fill="#010101"><path d="m15 7.6a1.1 1.1 0 0 1 .2.6v1.3h1.8v-1.3a1.6 1.6 0 0 0 -.2-1 1.5 1.5 0 0 0 -.6-.9 1.6 1.6 0 0 0 -.8-.6l-1.1-.2h-7.3v-2.4a.9.9 0 0 0 -.3-.6.8.8 0 0 0 -.6-.2l-.6.2c-.1.2-.3.4-.3.6v2.4h-2.4a.5.5 0 0 0 -.5.3.9.9 0 0 0 -.3.6.7.7 0 0 0 .3.6.5.5 0 0 0 .5.3h11.5a.9.9 0 0 1 .7.3z"/><path d="m11.2 15.5h-3.3a.9.9 0 0 1 -.6-.3.6.6 0 0 1 -.3-.6v-5.1a.9.9 0 0 0 -.3-.6.8.8 0 0 0 -.6-.2l-.6.2c-.1.2-.3.4-.3.6v5.1a2.7 2.7 0 0 0 2.7 2.7h3.3z"/><path d="m21.1 15.9h-4.1v-2.5a2.5 2.5 0 1 0 -5 0v1.6h1.3v-1.6a1.2 1.2 0 1 1 2.4 0v2.5h-.4a.7.7 0 0 0 -.8.8v4.2a.7.7 0 0 0 .8.8h5.8a.8.8 0 0 0 .9-.8v-4.2a.8.8 0 0 0 -.9-.8z"/></g></svg>

After

Width:  |  Height:  |  Size: 699 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,0A12,12,0,1,0,24,12,12,12,0,0,0,12,0ZM6.9,21.2l2-8.2h6.2l2,8.2A10.994,10.994,0,0,1,12,22.5,10.994,10.994,0,0,1,6.9,21.2Zm11.6-.9-1.9-7.8a1.4,1.4,0,0,0-1.3-1h-.1L12.9,6.6a1,1,0,0,0-1.8,0L8.8,11.5H8.7a1.4,1.4,0,0,0-1.3,1L5.5,20.3a10.7,10.7,0,0,1-4-8.3,10.5,10.5,0,0,1,21,0A10.7,10.7,0,0,1,18.5,20.3Z"/></svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@ -0,0 +1 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m9 7.815v11.685h-6.457zm.741-3.658a.725.725 0 0 0 -.647.388l-8.479 15.342a.75.75 0 0 0 .656 1.113h9.229v-16.092a.744.744 0 0 0 -.759-.751zm12.601 13.843-.829-1.5h-8.013v1.5zm-8.842-4.5v1.5h7.184l-.829-1.5zm2.211-7.5-.8-1.455a.75.75 0 0 0 -1.406.363v1.092zm-2.211 1.5v1.5h3.868l-.829-1.5zm0 3v1.5h5.526l-.826-1.5zm9.671 9h-9.671v1.5h9.229a.75.75 0 0 0 .656-1.113z"/></svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 20 20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m10 0c-5.514 0-10 4.486-10 10s4.486 10 10 10 10-4.486 10-10-4.486-10-10-10zm-2.455 11.028h4.91l1.6 6.48c-1.29.671-2.653 1.011-4.055 1.011-1.361 0-2.798-.359-4.054-1.011zm6.362-.637c-.095-.572-.649-.926-1.109-.926h-.084l-1.784-3.971c-.177-.309-.509-.494-.889-.494s-.712.185-.895.505l-1.778 3.96h-.084c-.562 0-1.019.38-1.108.917l-1.519 6.235c-1.969-1.57-3.094-3.971-3.094-6.618-.039-2.255.81-4.382 2.391-5.99 1.603-1.631 3.749-2.528 6.046-2.528 4.697 0 8.519 3.821 8.519 8.519 0 2.536-1.098 4.875-3.096 6.608z"/></svg>

After

Width:  |  Height:  |  Size: 619 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10,20A10,10,0,1,1,20,10,10.011,10.011,0,0,1,10,20ZM10,1.485A8.515,8.515,0,1,0,18.515,10,8.525,8.525,0,0,0,10,1.485Z"/><circle cx="10" cy="10" r="6.234"/></svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17.072,2.929A10,10,0,1,0,20,10,9.933,9.933,0,0,0,17.072,2.929Zm-2.684,7.793a3.254,3.254,0,0,0-1.28-1.147l0-4.3a.569.569,0,0,0-.03-.339.566.566,0,0,0-.553-.347.524.524,0,0,0-.293.106l-5,2.54a.567.567,0,0,0-.26.227.547.547,0,0,0-.078.324V9.575a3.276,3.276,0,0,0-1.806,2.886v4.465A8.552,8.552,0,0,1,1.539,10,8.461,8.461,0,0,1,15.983,4.016,8.4,8.4,0,0,1,18.462,10a8.547,8.547,0,0,1-3.548,6.927V12.458A3.257,3.257,0,0,0,14.388,10.722Zm-7.68,1.739A1.654,1.654,0,0,1,8.36,10.809h3.282a1.641,1.641,0,0,1,1.167.484,1.665,1.665,0,0,1,.485,1.168v5.316A8.208,8.208,0,0,1,10,18.461H9.944a8.038,8.038,0,0,1-3.236-.684Z"/></svg>

After

Width:  |  Height:  |  Size: 683 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m9.959 20.035a10 10 0 1 1 10-10 10.01 10.01 0 0 1 -10 10zm0-18.5a8.5 8.5 0 1 0 8.5 8.5 8.512 8.512 0 0 0 -8.5-8.502z"/><circle cx="10" cy="10" r="1.75"/></svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10,20A10,10,0,1,1,20,10,10.011,10.011,0,0,1,10,20ZM10,1.474A8.526,8.526,0,1,0,18.526,10,8.536,8.536,0,0,0,10,1.474Z"/><circle cx="10" cy="10" r="2.513"/></svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10,20A10,10,0,1,1,20,10,10.011,10.011,0,0,1,10,20ZM10,1.483A8.517,8.517,0,1,0,18.517,10,8.526,8.526,0,0,0,10,1.483Z"/><circle cx="10" cy="10" r="3.747"/></svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3,14.688C3,11.207,5.467,8.8,9.81,8.8h6.4l2.229.1-1.8-1.475-2.752-2.57a.749.749,0,0,1-.229-.555.781.781,0,0,1,.848-.8.951.951,0,0,1,.609.255l5.6,5.257a.785.785,0,0,1,0,1.212l-5.6,5.256a.906.906,0,0,1-.609.255.781.781,0,0,1-.848-.8.753.753,0,0,1,.229-.556l2.752-2.56,1.8-1.476-2.229.091H9.667c-3.248,0-4.962,1.713-4.962,4.164s1.714,4.281,4.962,4.281h2.19a.813.813,0,1,1,0,1.622h-2.2C5.4,20.5,3,18.177,3,14.688Z"/></svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@ -0,0 +1 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m14 3h-1.714l1.4-.819.906-.906-1.057-1.061-3.535 3.536 3.535 3.536 1.065-1.061-.906-.906-1.4-.819h1.706a6.508 6.508 0 0 1 6.5 6.5v1h1.5v-1a8.009 8.009 0 0 0 -8-8zm-1 7.5a1.5 1.5 0 0 1 1.5 1.5v7a1.5 1.5 0 0 1 -1.5 1.5h-7a1.5 1.5 0 0 1 -1.5-1.5v-7a1.5 1.5 0 0 1 1.5-1.5zm0-1.5h-7a3 3 0 0 0 -3 3v7a3 3 0 0 0 3 3h7a3 3 0 0 0 3-3v-7a3 3 0 0 0 -3-3z"/></svg>

After

Width:  |  Height:  |  Size: 444 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10,9c0,1.105-.672,2-1.5,2S7,10.105,7,9s.672-2,1.5-2S10,7.9,10,9Zm5.5-2c-.828,0-1.5.9-1.5,2s.672,2,1.5,2S17,10.105,17,9,16.328,7,15.5,7ZM22,9.692v2.257A3,3,0,0,1,20.734,14.4L10.482,21.456a3,3,0,0,1-1.7.53h-.1a7.322,7.322,0,0,1-3.61-.786A5.55,5.55,0,0,1,2.8,18.928c-.522-.977-.8-1.947-.8-4.62V9.692c0-2.673.278-3.643.8-4.62A5.55,5.55,0,0,1,5.072,2.8c.977-.522,1.947-.8,4.62-.8h4.616c2.673,0,3.643.278,4.62.8A5.55,5.55,0,0,1,21.2,5.072C21.722,6.049,22,7.019,22,9.692ZM8.743,20.485h.01a13.154,13.154,0,0,1,.352-3.464,6.521,6.521,0,0,1-2.6-2.053.75.75,0,0,1,1.188-.916l.019.026a5.025,5.025,0,0,0,1.967,1.57,6.165,6.165,0,0,1,2.532-2.507c1.1-.59,2.176-.89,4.975-.89h3.273a1.518,1.518,0,0,0,.036-.3V9.692c0-2.55-.258-3.23-.623-3.913a4.017,4.017,0,0,0-1.656-1.656C17.533,3.758,16.853,3.5,14.3,3.5H9.692c-2.55,0-3.23.258-3.913.623A4.017,4.017,0,0,0,4.123,5.779C3.758,6.461,3.5,7.142,3.5,9.692v4.616c0,2.55.258,3.23.623,3.913a4.017,4.017,0,0,0,1.656,1.656,5.822,5.822,0,0,0,2.96.608ZM19.028,13.75H17.191c-2.719,0-3.513.309-4.268.712a4.7,4.7,0,0,0-1.96,1.961,6.625,6.625,0,0,0-.7,3.362Z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +1 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m.5 18.5 5.325-14.5h1.813l5.326 14.5h-1.841l-1.459-4.1h-5.864l-1.459 4.1zm8.612-5.666-2.323-6.534h-.114l-2.323 6.538zm5.188 2.582c0-2.492 2.2-2.939 4.22-3.2 1.984-.255 2.8-.184 2.8-.991v-.057a2.02 2.02 0 0 0 -2.294-2.21 2.94 2.94 0 0 0 -2.826 1.642l-1.586-.567a4.418 4.418 0 0 1 4.363-2.549c1.387 0 4.022.4 4.022 3.852v7.164h-1.671v-1.469h-.084a3.419 3.419 0 0 1 -3.23 1.728c-2.068 0-3.714-1.218-3.714-3.343zm7.024-.85v-1.529c-.282.34-2.18.538-2.888.623-1.3.17-2.465.567-2.465 1.841 0 1.162.962 1.756 2.294 1.756a2.791 2.791 0 0 0 3.063-2.691z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M.5,18.5,5.825,4H7.638l5.326,14.5H11.123L9.664,14.4H3.8L2.341,18.5Zm8.612-5.666L6.789,6.3H6.675L4.352,12.838ZM14.3,15.416c0-2.492,2.2-2.939,4.22-3.2,1.984-.255,2.8-.184,2.8-.991v-.057a2.019,2.019,0,0,0-1.8-2.219,2.049,2.049,0,0,0-.5.009A2.94,2.94,0,0,0,16.2,10.6l-1.586-.567a4.417,4.417,0,0,1,4.363-2.549c1.387,0,4.022.4,4.022,3.852V18.5H21.328V17.031h-.084a3.419,3.419,0,0,1-3.23,1.728C15.946,18.759,14.3,17.541,14.3,15.416Zm7.024-.85V13.037c-.282.34-2.18.538-2.888.623-1.3.17-2.465.567-2.465,1.841,0,1.162.962,1.756,2.294,1.756a2.791,2.791,0,0,0,3.051-2.5c.006-.062.01-.125.012-.187Z"/></svg>

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 663 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M14.555-.016H5.414a5.43,5.43,0,0,0-5.43,5.43h0v9.141a5.429,5.429,0,0,0,5.43,5.429h9.141a5.428,5.428,0,0,0,5.429-5.429V5.414a5.429,5.429,0,0,0-5.429-5.43ZM15.378,6.6a.579.579,0,0,1-.6.556H11.355v8.854a.554.554,0,0,1-.555.555H9.2a.556.556,0,0,1-.556-.554h0V7.157H5.215a.577.577,0,0,1-.6-.555V5a.577.577,0,0,1,.6-.555H14.78a.578.578,0,0,1,.6.555Z"/></svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M14.905,4.1a.359.359,0,0,1,.359.359v1.92a.359.359,0,0,1-.359.359H11.32v8.8a.36.36,0,0,1-.358.36H9.04a.36.36,0,0,1-.358-.36v-8.8H5.1a.359.359,0,0,1-.359-.359V4.459A.359.359,0,0,1,5.1,4.1h9.81m0-1.6H5.1A1.961,1.961,0,0,0,3.136,4.459v1.92A1.962,1.962,0,0,0,5.1,8.342H7.079v7.2A1.961,1.961,0,0,0,9.04,17.5h1.92a1.96,1.96,0,0,0,1.958-1.959v-7.2H14.9a1.963,1.963,0,0,0,1.961-1.959V4.462A1.962,1.962,0,0,0,14.905,2.5Z"/></svg>

After

Width:  |  Height:  |  Size: 488 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M14.513,0H5.487A5.493,5.493,0,0,0,0,5.487v9.026A5.493,5.493,0,0,0,5.487,20h9.026A5.493,5.493,0,0,0,20,14.513V5.487A5.493,5.493,0,0,0,14.513,0Zm4,5.487v9.026a4.011,4.011,0,0,1-4,4H5.487a4.011,4.011,0,0,1-4-4V5.487a4.011,4.011,0,0,1,4-4h9.026A4.011,4.011,0,0,1,18.512,5.487Z"/><path d="M14.44,4.709H5.586a.628.628,0,0,0-.627.628V6.393a.63.63,0,0,0,.627.629H8.875V15.58a.628.628,0,0,0,.628.626h1.02a.627.627,0,0,0,.626-.626V7.022h3.29a.629.629,0,0,0,.628-.629V5.337A.629.629,0,0,0,14.44,4.709Z"/></svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@ -1 +1 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m12.073 2.5a9.438 9.438 0 0 0 -6.717 2.782c-.39.39-.7.707-.955.963l.349-1.307v-2.381h-1.5v5.943h5.943v-1.5h-2.381l-1.422.379c.26-.265.589-.6 1.027-1.036a8 8 0 1 1 5.656 13.657h-.573v1.5h.573a9.5 9.5 0 0 0 0-19z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21,14.688C21,11.207,18.533,8.8,14.191,8.8H7.79l-2.228.1,1.8-1.475,2.752-2.57a.749.749,0,0,0,.229-.555.781.781,0,0,0-.848-.8.951.951,0,0,0-.609.255l-5.6,5.257a.785.785,0,0,0,0,1.212l5.6,5.256a.906.906,0,0,0,.609.255.781.781,0,0,0,.848-.8.753.753,0,0,0-.229-.556l-2.752-2.56-1.8-1.476,2.228.091h6.543c3.248,0,4.962,1.713,4.962,4.164s-1.714,4.281-4.962,4.281h-2.19a.813.813,0,1,0,0,1.622h2.2C18.6,20.5,21,18.177,21,14.688Z"/></svg>

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 498 B

View File

@ -77,6 +77,7 @@
"@react-spring/web": "9.2.6",
"@signalapp/signal-client": "0.11.0",
"@sindresorhus/is": "0.8.0",
"@types/fabric": "4.5.3",
"abort-controller": "3.0.0",
"array-move": "2.1.0",
"axe-core": "4.1.4",
@ -94,6 +95,7 @@
"emoji-datasource-apple": "7.0.2",
"emoji-regex": "9.2.2",
"encoding": "0.1.13",
"fabric": "4.6.0",
"fast-glob": "3.2.1",
"filesize": "3.6.1",
"firstline": "1.2.1",

View File

@ -0,0 +1,129 @@
diff --git a/node_modules/@types/fabric/fabric-impl.d.ts b/node_modules/@types/fabric/fabric-impl.d.ts
index 9b2e307..6da58c3 100755
--- a/node_modules/@types/fabric/fabric-impl.d.ts
+++ b/node_modules/@types/fabric/fabric-impl.d.ts
@@ -1194,18 +1194,6 @@ interface IStaticCanvasOptions {
svgViewportTransformation?: boolean | undefined;
}
-export interface FreeDrawingBrush {
- /**
- * Can be any regular color value.
- */
- color: string;
-
- /**
- * Brush width measured in pixels.
- */
- width: number;
-}
-
export interface StaticCanvas
extends IObservable<StaticCanvas>,
IStaticCanvasOptions,
@@ -1222,7 +1210,7 @@ export class StaticCanvas {
_activeObject?: Object | Group | undefined;
- freeDrawingBrush: FreeDrawingBrush;
+ freeDrawingBrush: BaseBrush;
/**
* Calculates canvas element offset relative to the document
@@ -1931,6 +1919,8 @@ interface ICanvasOptions extends IStaticCanvasOptions {
export interface Canvas extends StaticCanvas {}
export interface Canvas extends ICanvasOptions {}
export class Canvas {
+ toCanvasElement(options?: IDataURLOptions): HTMLCanvasElement;
+
/**
* Constructor
* @param element <canvas> element to initialize instance on
@@ -2043,9 +2033,8 @@ export class Canvas {
getSelectionElement(): HTMLCanvasElement;
/**
* Returns currently active object
- * @return {fabric.Object} active object
*/
- getActiveObject(): Object;
+ getActiveObject(): null | Object;
/**
* Returns an array with the current selected objects
* @return {fabric.Object} active object
@@ -3997,7 +3986,7 @@ interface IPathOptions extends IObjectOptions {
*/
path?: Point[] | undefined;
}
-export interface Path extends Object, IPathOptions {}
+export interface Path extends Object {}
export class Path {
/**
* Constructor
@@ -4006,6 +3995,8 @@ export class Path {
*/
constructor(path?: string | Point[], options?: IPathOptions);
+ path: Array<string>;
+
pathOffset: Point;
/**
@@ -5865,6 +5856,12 @@ export class PatternBrush extends PencilBrush {
createPath(pathData: string): Path;
}
export class PencilBrush extends BaseBrush {
+ /**
+ * PencilBrush class
+ * @param fabric.Canvas canvas
+ */
+ constructor(canvas: fabric.Canvas);
+
/**
* Converts points to SVG path
* @param points Array of points
@@ -5878,6 +5875,32 @@ export class PencilBrush extends BaseBrush {
createPath(pathData: string): Path;
}
+///////////////////////////////////////////////////////////////////////////////
+// Fabric controlsUtils Interface
+//////////////////////////////////////////////////////////////////////////////
+interface IControlsUtils {
+ scaleCursorStyleHandler(eventData: Event, control: fabric.Control, fabricObject: fabric.Object): string;
+ skewCursorStyleHandler(eventData: Event, control: fabric.Control, fabricObject: fabric.Object): string;
+ scaleSkewCursorStyleHandler(eventData: Event, control: fabric.Control, fabricObject: fabric.Object): string;
+ rotationWithSnapping(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ scalingEqually(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ scalingX(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ scalingY(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ scalingYOrSkewingX(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ scalingXOrSkewingY(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ changeWidth(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ skewHandlerX(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ skewHandlerY(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ dragHandler(eventData: Event, transform: Transform, x: number, y: number): boolean;
+ scaleOrSkewActionName(eventData: Event, control: fabric.Control, fabricObject: fabric.Object): string;
+ rotationStyleHandler(eventData: Event, control: fabric.Control, fabricObject: fabric.Object): string;
+ wrapWithFixedAnchor<T>(actionHandler: ((eventData: Event, transform: Transform, x: number, y: number) => T)): ((eventData: Event, transform: Transform, x: number, y: number) => T);
+ wrapWithFireEvent<T>(actionHandler: ((eventData: Event, transform: Transform, x: number, y: number) => T)): ((eventData: Event, transform: Transform, x: number, y: number) => T);
+ getLocalPoint(transform: Transform, originX: string, originY: string, x: number, y: number): fabric.Point;
+}
+
+export const controlsUtils: IControlsUtils;
+
///////////////////////////////////////////////////////////////////////////////
// Fabric util Interface
//////////////////////////////////////////////////////////////////////////////
@@ -6452,6 +6475,12 @@ interface IUtilMisc {
*/
isTransparent(ctx: CanvasRenderingContext2D, x: number, y: number, tolerance: number): boolean;
+ /**
+ * Join path commands to go back to svg format
+ * @param pathData fabricJS parsed path commands
+ */
+ joinPath(pathData: Array<string>): string;
+
/**
* reset an object transform state to neutral. Top and left are not accounted for
* @static

View File

@ -0,0 +1,22 @@
diff --git a/node_modules/fabric/dist/fabric.js b/node_modules/fabric/dist/fabric.js
index 86536ce..487151b 100644
--- a/node_modules/fabric/dist/fabric.js
+++ b/node_modules/fabric/dist/fabric.js
@@ -3306,16 +3306,7 @@ fabric.CommonMethods = {
}
(function () {
- var style = fabric.document.documentElement.style,
- selectProp = 'userSelect' in style
- ? 'userSelect'
- : 'MozUserSelect' in style
- ? 'MozUserSelect'
- : 'WebkitUserSelect' in style
- ? 'WebkitUserSelect'
- : 'KhtmlUserSelect' in style
- ? 'KhtmlUserSelect'
- : '';
+ var selectProp = 'userSelect';
/**
* Makes element unselectable

View File

@ -3188,6 +3188,40 @@ button.module-image__border-overlay:focus {
position: relative;
}
.module-attachments__edit-icon {
align-items: center;
background: $color-black-alpha-60;
border-radius: 100%;
display: flex;
height: 36px;
justify-content: center;
left: 50%;
margin-left: -20px;
margin-top: -18px;
position: absolute;
top: 50%;
visibility: hidden;
width: 36px;
&::after {
@include color-svg('../images/icons/v2/edit-solid-16.svg', $color-white);
content: '';
height: 20px;
width: 20px;
}
}
.module-attachments--editable {
display: inline-block;
position: relative;
&:hover {
.module-attachments__edit-icon {
visibility: visible;
}
}
}
.module-attachments__close-button {
@include button-reset;
@ -5623,9 +5657,9 @@ button.module-image__border-overlay:focus {
}
@include dark-theme {
background: $color-gray-75;
background: $color-gray-80;
::-webkit-scrollbar-thumb {
border: 2px solid $color-gray-75;
border: 2px solid $color-gray-80;
}
}
}

View File

@ -0,0 +1,116 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.ContextMenu {
&__popper {
@extend %module-composition-popper;
margin: 0;
padding: 6px 0;
width: auto;
}
&__title {
@include font-body-1-bold;
margin-bottom: 12px;
}
&__button {
@include button-reset();
align-items: center;
border-radius: 16px;
display: flex;
height: 32px;
justify-content: center;
opacity: 0.5;
width: 32px;
&:focus,
&:hover {
opacity: 1;
}
&::after {
content: '';
display: block;
flex-shrink: 0;
height: 24px;
width: 24px;
}
&--active {
opacity: 1;
@include light-theme() {
background-color: $color-gray-05;
}
@include dark-theme() {
background-color: $color-gray-75;
}
}
}
&__option {
@include button-reset();
@include font-body-2;
@include dark-theme {
color: $color-gray-05;
}
align-items: center;
border-radius: 6px;
display: flex;
justify-content: space-between;
padding: 6px 8px;
min-width: 150px;
&--container {
display: flex;
}
&--icon {
height: 16px;
margin-right: 8px;
width: 16px;
}
&--selected {
height: 12px;
margin: 0 6px;
width: 16px;
@include light-theme {
@include color-svg('../images/icons/v2/check-24.svg', $color-black);
}
@include dark-theme {
@include color-svg('../images/icons/v2/check-24.svg', $color-white);
}
}
&--title {
@include font-body-2;
}
&--description {
@include font-subtitle;
}
&:hover {
@include light-theme() {
background-color: $color-gray-05;
}
@include dark-theme() {
background-color: $color-gray-65;
}
}
&--focused,
&:focus,
&:active {
border-radius: 6px;
box-shadow: 0 0 1px 1px $color-ultramarine;
outline: none;
}
}
}

View File

@ -0,0 +1,340 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.MediaEditor {
background: $color-gray-95;
display: flex;
flex-direction: column;
height: 100vh;
left: 0;
position: absolute;
top: 0;
user-select: none;
width: 100vw;
z-index: 2;
&__container {
display: flex;
flex: 1;
padding: 22px 60px;
padding-bottom: 0;
overflow: hidden;
}
&__media {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
position: relative;
width: 100%;
&--canvas {
border-radius: 12px;
transition: border-radius 200ms ease-out;
&--cropping {
border-radius: 0;
}
}
}
&__control {
@include button-reset;
align-items: center;
border-radius: 32px;
display: inline-flex;
height: 32px;
justify-content: center;
margin: 0 15px;
opacity: 1;
width: 32px;
&::after {
content: ' ';
height: 24px;
width: 24px;
}
&--crop::after {
@include color-svg('../images/icons/v2/crop-24.svg', $color-white);
}
&--pen::after {
@include color-svg('../images/icons/v2/draw-24.svg', $color-white);
}
&--redo {
&::after {
@include color-svg('../images/icons/v2/redo-24.svg', $color-white);
}
&:disabled::after {
@include color-svg('../images/icons/v2/redo-24.svg', $color-gray-45);
}
}
&--sticker.module-sticker-button__button::after {
@include color-svg(
'../images/icons/v2/sticker-smiley-24.svg',
$color-white
);
}
&--text::after {
@include color-svg('../images/icons/v2/text-24.svg', $color-white);
}
&--undo {
&::after {
@include color-svg('../images/icons/v2/undo-24.svg', $color-white);
}
&:disabled::after {
@include color-svg('../images/icons/v2/undo-24.svg', $color-gray-45);
}
}
&--selected {
background-color: $color-white;
&::after {
background-color: $color-black;
}
}
&:hover {
background-color: $color-gray-80;
&::after {
background-color: $color-white;
}
}
}
&__toolbar {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
padding: 22px;
width: 100%;
&--buttons {
align-items: center;
display: flex;
justify-content: center;
width: 100%;
}
&--space {
height: 36px;
margin-bottom: 22px;
}
}
&__controls {
display: flex;
flex-grow: 1;
flex-wrap: wrap;
justify-content: center;
max-width: 596px;
}
&__tools {
align-items: center;
display: flex;
height: 36px;
justify-content: center;
margin-bottom: 22px;
}
&__crop-toolbar {
align-items: center;
background-color: $color-gray-90;
border-radius: 10px;
color: $color-white;
display: flex;
&--button {
@include button-reset;
margin: 0 8px;
padding: 8px;
}
&--rotate {
@include color-svg(
'../images/icons/v2/rotate-outline-24.svg',
$color-white
);
height: 20px;
width: 20px;
}
&--flip {
@include color-svg(
'../images/icons/v2/flip-outline-24.svg',
$color-white
);
height: 20px;
width: 20px;
}
&--locked {
@include color-svg('../images/icons/v2/crop-lock-24.svg', $color-white);
height: 20px;
width: 20px;
}
&--unlocked {
@include color-svg('../images/icons/v2/crop-unlock-24.svg', $color-white);
height: 20px;
width: 20px;
}
&--reset {
padding-left: 24px;
}
&--crop {
padding-right: 24px;
}
}
&__hue-slider.Slider {
background-image: linear-gradient(
90deg,
hsl(0, 0%, 100%),
hsl(0, 0%, 0%),
hsl(0, 100%, 50%),
hsl(45, 100%, 50%),
hsl(90, 100%, 50%),
hsl(135, 100%, 50%),
hsl(180, 100%, 50%),
hsl(225, 100%, 50%),
hsl(270, 100%, 50%),
hsl(315, 100%, 50%),
hsl(360, 100%, 50%)
);
border-radius: 4px;
height: 8px;
margin-right: 7px;
width: 280px;
}
&__hue-slider__handle.Slider__handle {
background-color: transparent;
border: 7px solid $color-white;
margin-top: -7px;
margin-left: -11px;
height: 22px;
width: 22px;
}
&__button {
@mixin button($svg) {
height: 20px;
margin: 0 7px;
opacity: 1;
width: 20px;
&::after {
@include color-svg($svg, $color-white);
width: 20px;
height: 20px;
}
&:hover {
background-color: $color-gray-80;
}
}
&__text {
@include button('../images/icons/v2/edit-solid-16.svg');
}
&--draw-pen {
@include button('../images/icons/v2/pen-20.svg');
}
&--draw-highlighter {
@include button('../images/icons/v2/pen-highlighter-20.svg');
}
&--text-regular {
@include button('../images/icons/v2/text-regular-20.svg');
}
&--text-highlight {
@include button('../images/icons/v2/text-highlight-20.svg');
}
&--text-outline {
@include button('../images/icons/v2/text-outline-20.svg');
}
&--width-thin {
@include button('../images/icons/v2/pen-light-20.svg');
}
&--width-regular {
@include button('../images/icons/v2/pen-regular-20.svg');
}
&--width-medium {
@include button('../images/icons/v2/pen-medium-20.svg');
}
&--width-heavy {
@include button('../images/icons/v2/pen-heavy-20.svg');
}
}
&__icon {
&--draw-pen {
@include color-svg('../images/icons/v2/pen-20.svg', $color-white);
}
&--draw-highlighter {
@include color-svg(
'../images/icons/v2/pen-highlighter-20.svg',
$color-white
);
}
&--text-regular {
@include color-svg(
'../images/icons/v2/text-regular-20.svg',
$color-white
);
}
&--text-highlight {
@include color-svg(
'../images/icons/v2/text-highlight-20.svg',
$color-white
);
}
&--text-outline {
@include color-svg(
'../images/icons/v2/text-outline-20.svg',
$color-white
);
}
&--width-thin {
@include color-svg('../images/icons/v2/pen-light-20.svg', $color-white);
}
&--width-regular {
@include color-svg('../images/icons/v2/pen-regular-20.svg', $color-white);
}
&--width-medium {
@include color-svg('../images/icons/v2/pen-medium-20.svg', $color-white);
}
&--width-heavy {
@include color-svg('../images/icons/v2/pen-heavy-20.svg', $color-white);
}
}
}

View File

@ -51,6 +51,7 @@
@import './components/ContactPills.scss';
@import './components/ContactSpoofingReviewDialog.scss';
@import './components/ContactSpoofingReviewDialogPerson.scss';
@import './components/ContextMenu.scss';
@import './components/ConversationDetails.scss';
@import './components/ConversationHeader.scss';
@import './components/ConversationView.scss';
@ -71,6 +72,7 @@
@import './components/LeftPaneDialog.scss';
@import './components/LeftPaneSearchInput.scss';
@import './components/Lightbox.scss';
@import './components/MediaEditor.scss';
@import './components/MediaQualitySelector.scss';
@import './components/MessageAudio.scss';
@import './components/MessageBody.scss';

View File

@ -5,7 +5,9 @@ import type { CSSProperties, MouseEventHandler, ReactNode } from 'react';
import React from 'react';
import classNames from 'classnames';
import type { Theme } from '../util/theme';
import { assert } from '../util/assert';
import { themeClassName } from '../util/theme';
export enum ButtonSize {
Large,
@ -41,6 +43,7 @@ type PropsType = {
size?: ButtonSize;
style?: CSSProperties;
tabIndex?: number;
theme?: Theme;
variant?: ButtonVariant;
} & (
| {
@ -97,6 +100,7 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
icon,
style,
tabIndex,
theme,
variant = ButtonVariant.Primary,
size = variant === ButtonVariant.Details
? ButtonSize.Small
@ -120,7 +124,7 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
const variantClassName = VARIANT_CLASS_NAMES.get(variant);
assert(variantClassName, '<Button> variant not found');
return (
const buttonElement = (
<button
aria-label={ariaLabel}
className={classNames(
@ -142,5 +146,11 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
{children}
</button>
);
if (theme) {
return <div className={themeClassName(theme)}>{buttonElement}</div>;
}
return buttonElement;
}
);

View File

@ -55,6 +55,10 @@ import {
useAttachFileShortcut,
useKeyboardShortcuts,
} from '../hooks/useKeyboardShortcuts';
import { MediaEditor } from './MediaEditor';
import { IMAGE_PNG } from '../types/MIME';
import { isImageTypeSupported } from '../util/GoogleChrome';
import { canEditImages } from '../util/canEditImages';
export type CompositionAPIType =
| {
@ -253,6 +257,9 @@ export const CompositionArea = ({
const [disabled, setDisabled] = useState(false);
const [dirty, setDirty] = useState(false);
const [large, setLarge] = useState(false);
const [attachmentToEdit, setAttachmentToEdit] = useState<
AttachmentDraftType | undefined
>();
const inputApiRef = useRef<InputApi | undefined>();
const fileInputRef = useRef<null | HTMLInputElement>(null);
@ -286,6 +293,19 @@ export const CompositionArea = ({
}
}, []);
const hasImageEditingEnabled = canEditImages();
function maybeEditAttachment(attachment: AttachmentDraftType) {
if (
!hasImageEditingEnabled ||
!isImageTypeSupported(attachment.contentType)
) {
return;
}
setAttachmentToEdit(attachment);
}
const attachFileShortcut = useAttachFileShortcut(launchAttachmentPicker);
useKeyboardShortcuts(attachFileShortcut);
@ -560,6 +580,26 @@ export const CompositionArea = ({
return (
<div className="CompositionArea">
{attachmentToEdit && 'url' in attachmentToEdit && attachmentToEdit.url && (
<MediaEditor
i18n={i18n}
imageSrc={attachmentToEdit.url}
onClose={() => setAttachmentToEdit(undefined)}
onDone={data => {
const newAttachment = {
...attachmentToEdit,
contentType: IMAGE_PNG,
data,
size: data.byteLength,
};
addAttachment(conversationId, newAttachment);
setAttachmentToEdit(undefined);
}}
installedPacks={installedPacks}
recentStickers={recentStickers}
/>
)}
<div className="CompositionArea__toggle-large">
<button
type="button"
@ -607,8 +647,10 @@ export const CompositionArea = ({
<div className="CompositionArea__attachment-list">
<AttachmentList
attachments={draftAttachments}
canEditImages={hasImageEditingEnabled}
i18n={i18n}
onAddAttachment={launchAttachmentPicker}
onClickAttachment={maybeEditAttachment}
onClose={onClearAttachments}
onCloseAttachment={attachment => {
if (attachment.path) {

View File

@ -0,0 +1,39 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import type { PropsType } from './ContextMenu';
import { ContextMenu } from './ContextMenu';
import enMessages from '../../_locales/en/messages.json';
import { setupI18n } from '../util/setupI18n';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/ContextMenu', module);
const getDefaultProps = (): PropsType<number> => ({
i18n,
menuOptions: [
{
label: '1',
value: 1,
},
{
label: '2',
value: 2,
},
{
label: '3',
value: 3,
},
],
onChange: action('onChange'),
value: 1,
});
story.add('Default', () => {
return <ContextMenu {...getDefaultProps()} />;
});

View File

@ -0,0 +1,186 @@
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { KeyboardEvent } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import classNames from 'classnames';
import { usePopper } from 'react-popper';
import { noop } from 'lodash';
import type { Theme } from '../util/theme';
import type { LocalizerType } from '../types/Util';
import { themeClassName } from '../util/theme';
type OptionType<T> = {
readonly icon?: string;
readonly label: string;
readonly description?: string;
readonly value: T;
};
export type PropsType<T> = {
readonly buttonClassName?: string;
readonly i18n: LocalizerType;
readonly menuOptions: ReadonlyArray<OptionType<T>>;
readonly onChange: (value: T) => unknown;
readonly theme?: Theme;
readonly title?: string;
readonly value: T;
};
export const ContextMenu = <T extends unknown>({
buttonClassName,
i18n,
menuOptions,
onChange,
theme,
title,
value,
}: PropsType<T>): JSX.Element => {
const [menuShowing, setMenuShowing] = useState<boolean>(false);
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(
undefined
);
// We use regular MouseEvent below, and this one uses React.MouseEvent
const handleClick = (ev: KeyboardEvent | React.MouseEvent) => {
setMenuShowing(true);
ev.stopPropagation();
ev.preventDefault();
};
const handleKeyDown = (ev: KeyboardEvent) => {
if (!menuShowing) {
if (ev.key === 'Enter') {
setFocusedIndex(0);
}
return;
}
if (ev.key === 'ArrowDown') {
const currFocusedIndex = focusedIndex || 0;
const nextFocusedIndex =
currFocusedIndex >= menuOptions.length - 1 ? 0 : currFocusedIndex + 1;
setFocusedIndex(nextFocusedIndex);
ev.stopPropagation();
ev.preventDefault();
}
if (ev.key === 'ArrowUp') {
const currFocusedIndex = focusedIndex || 0;
const nextFocusedIndex =
currFocusedIndex === 0 ? menuOptions.length - 1 : currFocusedIndex - 1;
setFocusedIndex(nextFocusedIndex);
ev.stopPropagation();
ev.preventDefault();
}
if (ev.key === 'Enter') {
if (focusedIndex !== undefined) {
onChange(menuOptions[focusedIndex].value);
}
setMenuShowing(false);
ev.stopPropagation();
ev.preventDefault();
}
};
const handleClose = useCallback(() => {
setMenuShowing(false);
setFocusedIndex(undefined);
}, [setMenuShowing]);
const [referenceElement, setReferenceElement] =
useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
null
);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'top-start',
strategy: 'fixed',
});
useEffect(() => {
if (!menuShowing) {
return noop;
}
const handleOutsideClick = (event: MouseEvent) => {
if (!referenceElement?.contains(event.target as Node)) {
handleClose();
event.stopPropagation();
event.preventDefault();
}
};
document.addEventListener('click', handleOutsideClick);
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [menuShowing, handleClose, referenceElement]);
return (
<div className={theme ? themeClassName(theme) : undefined}>
<button
aria-label={i18n('ContextMenu--button')}
className={classNames(buttonClassName, {
ContextMenu__button: true,
'ContextMenu__button--active': menuShowing,
})}
onClick={handleClick}
onKeyDown={handleKeyDown}
ref={setReferenceElement}
type="button"
/>
{menuShowing && (
<div
className="ContextMenu__popper"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{title && <div className="ContextMenu__title">{title}</div>}
{menuOptions.map((option, index) => (
<button
aria-label={option.label}
className={classNames({
ContextMenu__option: true,
'ContextMenu__option--focused': focusedIndex === index,
})}
key={option.label}
type="button"
onClick={() => {
onChange(option.value);
setMenuShowing(false);
}}
>
<div className="ContextMenu__option--container">
{option.icon && (
<div
className={classNames(
'ContextMenu__option--icon',
option.icon
)}
/>
)}
<div>
<div className="ContextMenu__option--title">
{option.label}
</div>
{option.description && (
<div className="ContextMenu__option--description">
{option.description}
</div>
)}
</div>
</div>
{value === option.value ? (
<div className="ContextMenu__option--selected" />
) : null}
</button>
))}
</div>
)}
</div>
);
};

View File

@ -430,12 +430,8 @@ story.add('Archive: searching a conversation', () => (
modeSpecificProps: {
mode: LeftPaneMode.Archive,
archivedConversations: defaultConversations,
searchConversation: defaultConversations[0],
searchTerm: 'foo bar',
conversationResults: { isLoading: true },
contactResults: { isLoading: true },
messageResults: { isLoading: true },
primarySendsSms: false,
searchConversation: undefined,
searchTerm: '',
},
})}
/>

View File

@ -0,0 +1,46 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import type { PropsType } from './MediaEditor';
import { MediaEditor } from './MediaEditor';
import enMessages from '../../_locales/en/messages.json';
import { setupI18n } from '../util/setupI18n';
import { Stickers, installedPacks } from '../test-both/helpers/getStickerPacks';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/MediaEditor', module);
const IMAGE_1 = '/fixtures/nathan-anderson-316188-unsplash.jpg';
const IMAGE_2 = '/fixtures/tina-rolf-269345-unsplash.jpg';
const IMAGE_3 = '/fixtures/kitten-4-112-112.jpg';
const IMAGE_4 = '/fixtures/snow.jpg';
const getDefaultProps = (): PropsType => ({
i18n,
imageSrc: IMAGE_2,
onClose: action('onClose'),
onDone: action('onDone'),
// StickerButtonProps
installedPacks,
recentStickers: [Stickers.wide, Stickers.tall, Stickers.abe],
});
story.add('Extra Large', () => <MediaEditor {...getDefaultProps()} />);
story.add('Large', () => (
<MediaEditor {...getDefaultProps()} imageSrc={IMAGE_1} />
));
story.add('Smol', () => (
<MediaEditor {...getDefaultProps()} imageSrc={IMAGE_3} />
));
story.add('Portrait', () => (
<MediaEditor {...getDefaultProps()} imageSrc={IMAGE_4} />
));

View File

@ -0,0 +1,934 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Measure from 'react-measure';
import React, { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { createPortal } from 'react-dom';
import { fabric } from 'fabric';
import { get, has, noop } from 'lodash';
import type { LocalizerType } from '../types/Util';
import type { Props as StickerButtonProps } from './stickers/StickerButton';
import type { ImageStateType } from '../mediaEditor/ImageStateType';
import * as log from '../logging/log';
import { Button, ButtonVariant } from './Button';
import { ContextMenu } from './ContextMenu';
import { Slider } from './Slider';
import { StickerButton } from './stickers/StickerButton';
import { Theme } from '../util/theme';
import { canvasToBytes } from '../util/canvasToBytes';
import { useFabricHistory } from '../mediaEditor/useFabricHistory';
import { usePortal } from '../hooks/usePortal';
import { useUniqueId } from '../hooks/useUniqueId';
import { MediaEditorFabricPencilBrush } from '../mediaEditor/MediaEditorFabricPencilBrush';
import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect';
import { MediaEditorFabricIText } from '../mediaEditor/MediaEditorFabricIText';
import { MediaEditorFabricSticker } from '../mediaEditor/MediaEditorFabricSticker';
import { getRGBA, getHSL } from '../mediaEditor/util/color';
import {
TextStyle,
getTextStyleAttributes,
} from '../mediaEditor/util/getTextStyleAttributes';
export type PropsType = {
i18n: LocalizerType;
imageSrc: string;
onClose: () => unknown;
onDone: (data: Uint8Array) => unknown;
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'>;
enum EditMode {
Crop = 'Crop',
Draw = 'Draw',
Text = 'Text',
}
enum DrawWidth {
Thin = 2,
Regular = 4,
Medium = 12,
Heavy = 24,
}
enum DrawTool {
Pen = 'Pen',
Highlighter = 'Highlighter',
}
export const MediaEditor = ({
i18n,
imageSrc,
onClose,
onDone,
// StickerButtonProps
installedPacks,
recentStickers,
}: PropsType): JSX.Element | null => {
const [fabricCanvas, setFabricCanvas] = useState<fabric.Canvas | undefined>();
const [image, setImage] = useState<HTMLImageElement>(new Image());
const isRestoringImageState = useRef(false);
const canvasId = useUniqueId();
const [imageState, setImageState] = useState<ImageStateType>({
angle: 0,
cropX: 0,
cropY: 0,
flipX: false,
flipY: false,
height: image.height,
width: image.width,
});
// Initial image load and Fabric canvas setup
useEffect(() => {
const img = new Image();
img.onload = () => {
setImage(img);
const canvas = new fabric.Canvas(canvasId);
canvas.selection = false;
setFabricCanvas(canvas);
setImageState(curr => ({
...curr,
height: img.height,
width: img.width,
}));
};
img.onerror = () => {
// This is a bad experience, but it should be impossible.
log.error('<MediaEditor>: image failed to load. Closing');
onClose();
};
img.src = imageSrc;
return () => {
img.onload = noop;
img.onerror = noop;
};
}, [canvasId, imageSrc, onClose]);
// Keyboard support
useEffect(() => {
function handleKeydown(ev: KeyboardEvent) {
if (!fabricCanvas) {
return;
}
const obj = fabricCanvas.getActiveObject();
if (!obj) {
return;
}
if (ev.key === 'Delete') {
if (!obj.excludeFromExport) {
fabricCanvas.remove(obj);
}
ev.preventDefault();
ev.stopPropagation();
}
if (ev.key === 'Escape') {
fabricCanvas.discardActiveObject();
fabricCanvas.requestRenderAll();
ev.preventDefault();
ev.stopPropagation();
}
}
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
};
}, [fabricCanvas]);
const history = useFabricHistory(fabricCanvas);
// Take a snapshot of history whenever imageState changes
useEffect(() => {
if (
!imageState.height ||
!imageState.width ||
isRestoringImageState.current
) {
isRestoringImageState.current = false;
return;
}
history?.takeSnapshot(imageState);
}, [history, imageState]);
const [containerWidth, setContainerWidth] = useState(0);
const [containerHeight, setContainerHeight] = useState(0);
const zoom =
Math.min(
containerWidth / imageState.width,
containerHeight / imageState.height
) || 1;
// Update the canvas dimensions (and therefore zoom)
useEffect(() => {
if (!fabricCanvas || !imageState.width || !imageState.height) {
return;
}
fabricCanvas.setDimensions({
width: imageState.width * zoom,
height: imageState.height * zoom,
});
fabricCanvas.setZoom(zoom);
}, [
containerHeight,
containerWidth,
fabricCanvas,
imageState.height,
imageState.width,
zoom,
]);
// Refresh the background image according to imageState changes
useEffect(() => {
const backgroundImage = new fabric.Image(image, {
canvas: fabricCanvas,
height: imageState.height || image.height,
width: imageState.width || image.width,
});
let left: number;
let top: number;
switch (imageState.angle) {
case 0:
left = 0;
top = 0;
break;
case 90:
left = imageState.width;
top = 0;
break;
case 180:
left = imageState.width;
top = imageState.height;
break;
case 270:
left = 0;
top = imageState.height;
break;
default:
throw new Error('Unexpected angle');
}
let { height, width } = imageState;
if (imageState.angle % 180) {
[width, height] = [height, width];
}
fabricCanvas?.setBackgroundImage(
backgroundImage,
fabricCanvas.requestRenderAll.bind(fabricCanvas),
{
angle: imageState.angle,
cropX: imageState.cropX,
cropY: imageState.cropY,
flipX: imageState.flipX,
flipY: imageState.flipY,
left,
top,
originX: 'left',
originY: 'top',
width,
height,
}
);
}, [fabricCanvas, image, imageState]);
const [canRedo, setCanRedo] = useState(false);
const [canUndo, setCanUndo] = useState(false);
const [cropAspectRatioLock, setcropAspectRatioLock] = useState(false);
const [drawTool, setDrawTool] = useState<DrawTool>(DrawTool.Pen);
const [drawWidth, setDrawWidth] = useState<DrawWidth>(DrawWidth.Regular);
const [editMode, setEditMode] = useState<EditMode | undefined>();
const [sliderValue, setSliderValue] = useState<number>(0);
const [textStyle, setTextStyle] = useState<TextStyle>(TextStyle.Regular);
// Check if we can undo/redo & restore the image state on undo/undo
useEffect(() => {
if (!history) {
return;
}
function refreshUndoState() {
if (!history) {
return;
}
setCanUndo(history.canUndo());
setCanRedo(history.canRedo());
}
function restoreImageState(prevImageState?: ImageStateType) {
if (prevImageState) {
isRestoringImageState.current = true;
setImageState(prevImageState);
}
}
history.on('historyChanged', refreshUndoState);
history.on('appliedState', restoreImageState);
return () => {
history.off('historyChanged', refreshUndoState);
history.off('appliedState', restoreImageState);
};
}, [history]);
// If you select a text path auto enter edit mode
useEffect(() => {
if (!fabricCanvas) {
return;
}
function updateEditMode(ev: fabric.IEvent) {
if (ev.target?.get('type') === 'MediaEditorFabricIText') {
setEditMode(EditMode.Text);
} else if (editMode === EditMode.Text) {
setEditMode(undefined);
}
}
fabricCanvas.on('selection:created', updateEditMode);
fabricCanvas.on('selection:updated', updateEditMode);
fabricCanvas.on('selection:cleared', updateEditMode);
return () => {
fabricCanvas.off('selection:created', updateEditMode);
fabricCanvas.off('selection:updated', updateEditMode);
fabricCanvas.off('selection:cleared', updateEditMode);
};
}, [editMode, fabricCanvas]);
// Ensure scaling is in locked|unlocked state only when cropping
useEffect(() => {
if (!fabricCanvas) {
return;
}
if (editMode === EditMode.Crop) {
fabricCanvas.uniformScaling = cropAspectRatioLock;
} else {
fabricCanvas.uniformScaling = true;
}
}, [cropAspectRatioLock, editMode, fabricCanvas]);
// Remove any blank text when edit mode changes off of text
useEffect(() => {
if (!fabricCanvas) {
return;
}
if (editMode !== EditMode.Text) {
const obj = fabricCanvas.getActiveObject();
if (obj && has(obj, 'text') && get(obj, 'text') === '') {
fabricCanvas.remove(obj);
}
}
}, [editMode, fabricCanvas]);
// Toggle draw mode
useEffect(() => {
if (!fabricCanvas) {
return;
}
if (editMode !== EditMode.Draw) {
fabricCanvas.isDrawingMode = false;
return;
}
fabricCanvas.discardActiveObject();
fabricCanvas.isDrawingMode = true;
const freeDrawingBrush = new MediaEditorFabricPencilBrush(fabricCanvas);
if (drawTool === DrawTool.Highlighter) {
freeDrawingBrush.color = getRGBA(sliderValue, 0.5);
freeDrawingBrush.strokeLineCap = 'square';
freeDrawingBrush.strokeLineJoin = 'miter';
freeDrawingBrush.width = (drawWidth / zoom) * 2;
} else {
freeDrawingBrush.color = getHSL(sliderValue);
freeDrawingBrush.strokeLineCap = 'round';
freeDrawingBrush.strokeLineJoin = 'bevel';
freeDrawingBrush.width = drawWidth / zoom;
}
fabricCanvas.freeDrawingBrush = freeDrawingBrush;
fabricCanvas.requestRenderAll();
}, [drawTool, drawWidth, editMode, fabricCanvas, sliderValue, zoom]);
// Change text style
useEffect(() => {
if (!fabricCanvas) {
return;
}
if (editMode !== EditMode.Text) {
return;
}
const obj = fabricCanvas.getActiveObject();
if (!obj || !(obj instanceof MediaEditorFabricIText)) {
return;
}
obj.set(getTextStyleAttributes(textStyle, sliderValue));
fabricCanvas.requestRenderAll();
}, [editMode, fabricCanvas, sliderValue, textStyle]);
// Create the CroppingRect
useEffect(() => {
if (!fabricCanvas) {
return;
}
if (editMode === EditMode.Crop) {
const PADDING = MediaEditorFabricCropRect.PADDING / zoom;
// For reasons we don't understand, height and width on small images doesn't work
// right (it bleeds out) so we decrease them for small images.
const height =
imageState.height - PADDING * Math.max(440 / imageState.height, 2);
const width =
imageState.width - PADDING * Math.max(440 / imageState.width, 2);
let rect: MediaEditorFabricCropRect;
const obj = fabricCanvas.getActiveObject();
if (obj instanceof MediaEditorFabricCropRect) {
rect = obj;
rect.set({ height, width, scaleX: 1, scaleY: 1 });
} else {
rect = new MediaEditorFabricCropRect({
height,
width,
});
rect.on('deselected', () => {
setEditMode(undefined);
});
fabricCanvas.add(rect);
fabricCanvas.setActiveObject(rect);
}
fabricCanvas.viewportCenterObject(rect);
rect.setCoords();
} else {
fabricCanvas.getObjects().forEach(obj => {
if (obj instanceof MediaEditorFabricCropRect) {
fabricCanvas.remove(obj);
}
});
}
}, [editMode, fabricCanvas, imageState.height, imageState.width, zoom]);
// In an ideal world we'd use <ModalHost /> to get the nice animation benefits
// but because of the way IText is implemented -- with a hidden textarea -- to
// capture keyboard events, we can't use ModalHost since that traps focus, and
// focus trapping doesn't play nice with fabric's IText.
const portal = usePortal();
if (!portal) {
return null;
}
let tooling: JSX.Element | undefined;
if (editMode === EditMode.Text) {
tooling = (
<>
<Slider
label={i18n('CustomColorEditor__hue')}
moduleClassName="MediaEditor__hue-slider"
onChange={setSliderValue}
value={sliderValue}
/>
<ContextMenu
buttonClassName={classNames('MediaEditor__button--text', {
'MediaEditor__button--text-regular':
textStyle === TextStyle.Regular,
'MediaEditor__button--text-highlight':
textStyle === TextStyle.Highlight,
'MediaEditor__button--text-outline':
textStyle === TextStyle.Outline,
})}
i18n={i18n}
menuOptions={[
{
icon: 'MediaEditor__icon--text-regular',
label: i18n('MediaEditor__text--regular'),
value: TextStyle.Regular,
},
{
icon: 'MediaEditor__icon--text-highlight',
label: i18n('MediaEditor__text--highlight'),
value: TextStyle.Highlight,
},
{
icon: 'MediaEditor__icon--text-outline',
label: i18n('MediaEditor__text--outline'),
value: TextStyle.Outline,
},
]}
onChange={value => setTextStyle(value)}
theme={Theme.Dark}
value={textStyle}
/>
</>
);
} else if (editMode === EditMode.Draw) {
tooling = (
<>
<Slider
label={i18n('CustomColorEditor__hue')}
moduleClassName="MediaEditor__hue-slider"
onChange={setSliderValue}
value={sliderValue}
/>
<ContextMenu
buttonClassName={classNames('MediaEditor__button--draw', {
'MediaEditor__button--draw-pen': drawTool === DrawTool.Pen,
'MediaEditor__button--draw-highlighter':
drawTool === DrawTool.Highlighter,
})}
i18n={i18n}
menuOptions={[
{
icon: 'MediaEditor__icon--draw-pen',
label: i18n('MediaEditor__draw--pen'),
value: DrawTool.Pen,
},
{
icon: 'MediaEditor__icon--draw-highlighter',
label: i18n('MediaEditor__draw--highlighter'),
value: DrawTool.Highlighter,
},
]}
onChange={value => setDrawTool(value)}
theme={Theme.Dark}
value={drawTool}
/>
<ContextMenu
buttonClassName={classNames('MediaEditor__button--width', {
'MediaEditor__button--width-thin': drawWidth === DrawWidth.Thin,
'MediaEditor__button--width-regular':
drawWidth === DrawWidth.Regular,
'MediaEditor__button--width-medium': drawWidth === DrawWidth.Medium,
'MediaEditor__button--width-heavy': drawWidth === DrawWidth.Heavy,
})}
i18n={i18n}
menuOptions={[
{
icon: 'MediaEditor__icon--width-thin',
label: i18n('MediaEditor__draw--thin'),
value: DrawWidth.Thin,
},
{
icon: 'MediaEditor__icon--width-regular',
label: i18n('MediaEditor__draw--regular'),
value: DrawWidth.Regular,
},
{
icon: 'MediaEditor__icon--width-medium',
label: i18n('MediaEditor__draw--medium'),
value: DrawWidth.Medium,
},
{
icon: 'MediaEditor__icon--width-heavy',
label: i18n('MediaEditor__draw--heavy'),
value: DrawWidth.Heavy,
},
]}
onChange={value => setDrawWidth(value)}
theme={Theme.Dark}
value={drawWidth}
/>
</>
);
} else if (editMode === EditMode.Crop) {
const canReset =
imageState.cropX !== 0 ||
imageState.cropY !== 0 ||
imageState.flipX ||
imageState.flipY ||
imageState.angle !== 0;
tooling = (
<div className="MediaEditor__crop-toolbar">
<button
aria-label={i18n('MediaEditor__crop--reset')}
className="MediaEditor__crop-toolbar--button MediaEditor__crop-toolbar--reset"
disabled={!canReset}
onClick={async () => {
if (!fabricCanvas) {
return;
}
setImageState({
angle: 0,
cropX: 0,
cropY: 0,
flipX: false,
flipY: false,
height: image.height,
width: image.width,
});
}}
type="button"
>
{i18n('MediaEditor__crop--reset')}
</button>
<button
aria-label={i18n('MediaEditor__crop--rotate')}
className="MediaEditor__crop-toolbar--button MediaEditor__crop-toolbar--rotate"
onClick={() => {
if (!fabricCanvas) {
return;
}
fabricCanvas.getObjects().forEach(obj => {
if (obj instanceof MediaEditorFabricCropRect) {
return;
}
const center = obj.getCenterPoint();
obj.set('angle', (imageState.angle + 270) % 360);
obj.setPositionByOrigin(
new fabric.Point(center.y, imageState.width - center.x),
'center',
'center'
);
obj.setCoords();
});
setImageState(curr => ({
...curr,
angle: (curr.angle + 270) % 360,
height: curr.width,
width: curr.height,
}));
}}
type="button"
/>
<button
aria-label={i18n('MediaEditor__crop--flip')}
className="MediaEditor__crop-toolbar--button MediaEditor__crop-toolbar--flip"
onClick={() => {
if (!fabricCanvas) {
return;
}
setImageState(curr => ({
...curr,
...(curr.angle % 180
? { flipY: !curr.flipY }
: { flipX: !curr.flipX }),
}));
}}
type="button"
/>
<button
aria-label={i18n('MediaEditor__crop--lock')}
className={classNames('MediaEditor__crop-toolbar--button', {
'MediaEditor__crop-toolbar--locked': cropAspectRatioLock,
'MediaEditor__crop-toolbar--unlocked': !cropAspectRatioLock,
})}
onClick={() => {
if (fabricCanvas) {
fabricCanvas.uniformScaling = !cropAspectRatioLock;
}
setcropAspectRatioLock(!cropAspectRatioLock);
}}
type="button"
/>
<button
aria-label={i18n('MediaEditor__crop--crop')}
className="MediaEditor__crop-toolbar--button MediaEditor__crop-toolbar--crop"
onClick={() => {
if (!fabricCanvas) {
return;
}
const cropRect = fabricCanvas.getActiveObject();
if (!(cropRect instanceof MediaEditorFabricCropRect)) {
return;
}
const { left, height, top, width } = cropRect.getBoundingRect(true);
setImageState(curr => {
let cropX: number;
let cropY: number;
switch (curr.angle) {
case 0:
cropX = curr.cropX + left;
cropY = curr.cropY + top;
break;
case 90:
cropX = curr.cropX + top;
cropY = curr.cropY + (curr.width - (left + width));
break;
case 180:
cropX = curr.cropX + (curr.width - (left + width));
cropY = curr.cropY + (curr.height - (top + height));
break;
case 270:
cropX = curr.cropX + (curr.height - (top + height));
cropY = curr.cropY + left;
break;
default:
throw new Error('Unexpected angle');
}
return {
...curr,
cropX,
cropY,
height,
width,
};
});
fabricCanvas.getObjects().forEach(obj => {
const { x, y } = obj.getCenterPoint();
const translatedCenter = new fabric.Point(x - left, y - top);
obj.setPositionByOrigin(translatedCenter, 'center', 'center');
obj.setCoords();
});
}}
type="button"
>
{i18n('MediaEditor__crop--crop')}
</button>
</div>
);
}
return createPortal(
<div className="MediaEditor">
<div className="MediaEditor__container">
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds) {
log.error('We should be measuring the bounds');
return;
}
setContainerWidth(bounds.width);
setContainerHeight(bounds.height);
}}
>
{({ measureRef }) => (
<div className="MediaEditor__media" ref={measureRef}>
{image && (
<div>
<canvas
className={classNames('MediaEditor__media--canvas', {
'MediaEditor__media--canvas--cropping':
editMode === EditMode.Crop,
})}
id={canvasId}
/>
</div>
)}
</div>
)}
</Measure>
</div>
<div className="MediaEditor__toolbar">
{tooling ? (
<div className="MediaEditor__tools">{tooling}</div>
) : (
<div className="MediaEditor__toolbar--space" />
)}
<div className="MediaEditor__toolbar--buttons">
<Button
onClick={onClose}
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('discard')}
</Button>
<div className="MediaEditor__controls">
<button
aria-label={i18n('MediaEditor__control--draw')}
className={classNames({
MediaEditor__control: true,
'MediaEditor__control--pen': true,
'MediaEditor__control--selected': editMode === EditMode.Draw,
})}
onClick={() => {
setEditMode(
editMode === EditMode.Draw ? undefined : EditMode.Draw
);
}}
type="button"
/>
<button
aria-label={i18n('MediaEditor__control--text')}
className={classNames({
MediaEditor__control: true,
'MediaEditor__control--text': true,
'MediaEditor__control--selected': editMode === EditMode.Text,
})}
onClick={() => {
if (!fabricCanvas) {
return;
}
if (editMode === EditMode.Text) {
setEditMode(undefined);
} else {
const FONT_SIZE_RELATIVE_TO_CANVAS = 10;
const fontSize =
Math.min(imageState.width, imageState.height) /
FONT_SIZE_RELATIVE_TO_CANVAS;
const text = new MediaEditorFabricIText('', {
...getTextStyleAttributes(textStyle, sliderValue),
fontSize,
});
text.setPositionByOrigin(
new fabric.Point(
imageState.width / 2,
imageState.height / 2
),
'center',
'center'
);
text.setCoords();
fabricCanvas.add(text);
fabricCanvas.setActiveObject(text);
text.enterEditing();
setEditMode(EditMode.Text);
}
}}
type="button"
/>
<StickerButton
blessedPacks={[]}
className={classNames({
MediaEditor__control: true,
'MediaEditor__control--sticker': true,
})}
clearInstalledStickerPack={noop}
clearShowIntroduction={() => {
// We're using this as a callback for when the sticker button
// is pressed.
fabricCanvas?.discardActiveObject();
setEditMode(undefined);
}}
clearShowPickerHint={noop}
i18n={i18n}
installedPacks={installedPacks}
knownPacks={[]}
onPickSticker={(_packId, _stickerId, src: string) => {
if (!fabricCanvas) {
return;
}
const STICKER_SIZE_RELATIVE_TO_CANVAS = 4;
const size =
Math.min(imageState.width, imageState.height) /
STICKER_SIZE_RELATIVE_TO_CANVAS;
const sticker = new MediaEditorFabricSticker(src);
sticker.scaleToHeight(size);
sticker.setPositionByOrigin(
new fabric.Point(imageState.width / 2, imageState.height / 2),
'center',
'center'
);
sticker.setCoords();
fabricCanvas.add(sticker);
fabricCanvas.setActiveObject(sticker);
setEditMode(undefined);
}}
receivedPacks={[]}
recentStickers={recentStickers}
showPickerHint={false}
theme={Theme.Dark}
/>
<button
aria-label={i18n('MediaEditor__control--crop')}
className={classNames({
MediaEditor__control: true,
'MediaEditor__control--crop': true,
'MediaEditor__control--selected': editMode === EditMode.Crop,
})}
onClick={() => {
if (!fabricCanvas) {
return;
}
if (editMode === EditMode.Crop) {
const obj = fabricCanvas.getActiveObject();
if (obj instanceof MediaEditorFabricCropRect) {
fabricCanvas.remove(obj);
}
setEditMode(undefined);
} else {
setEditMode(EditMode.Crop);
}
}}
type="button"
/>
<button
aria-label={i18n('MediaEditor__control--undo')}
className="MediaEditor__control MediaEditor__control--undo"
disabled={!canUndo}
onClick={() => {
if (editMode === EditMode.Crop) {
setEditMode(undefined);
}
history?.undo();
}}
type="button"
/>
<button
aria-label={i18n('MediaEditor__control--redo')}
className="MediaEditor__control MediaEditor__control--redo"
disabled={!canRedo}
onClick={() => {
if (editMode === EditMode.Crop) {
setEditMode(undefined);
}
history?.redo();
}}
type="button"
/>
</div>
<Button
onClick={async () => {
if (!fabricCanvas) {
return;
}
const renderedCanvas = fabricCanvas.toCanvasElement();
const data = await canvasToBytes(renderedCanvas);
onDone(data);
}}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('done')}
</Button>
</div>
</div>
</div>,
portal
);
};

View File

@ -43,7 +43,7 @@ story.add('One File', () => {
}),
],
});
return <AttachmentList {...props} />;
return <AttachmentList {...props} canEditImages />;
});
story.add('Multiple Visual Attachments', () => {

View File

@ -17,6 +17,7 @@ import {
export type Props = Readonly<{
attachments: ReadonlyArray<AttachmentDraftType>;
canEditImages?: boolean;
i18n: LocalizerType;
onAddAttachment?: () => void;
onClickAttachment?: (attachment: AttachmentDraftType) => void;
@ -41,6 +42,7 @@ function getUrl(attachment: AttachmentDraftType): string | undefined {
export const AttachmentList = ({
attachments,
canEditImages,
i18n,
onAddAttachment,
onClickAttachment,
@ -88,7 +90,7 @@ export const AttachmentList = ({
? () => onClickAttachment(attachment)
: undefined;
return (
const imgElement = (
<Image
key={key}
alt={i18n('stagedImageAttachment', [
@ -109,6 +111,17 @@ export const AttachmentList = ({
onError={closeAttachment}
/>
);
if (isImage && canEditImages) {
return (
<div className="module-attachments--editable">
{imgElement}
<div className="module-attachments__edit-icon" />
</div>
);
}
return imgElement;
}
return (

View File

@ -43,6 +43,8 @@ const searchResultKeys: Array<
'conversationResults' | 'contactResults' | 'messageResults'
> = ['conversationResults', 'contactResults', 'messageResults'];
/* eslint-disable class-methods-use-this */
export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType> {
private readonly conversationResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;

View File

@ -6,13 +6,17 @@ import classNames from 'classnames';
import { get, noop } from 'lodash';
import { Manager, Popper, Reference } from 'react-popper';
import { createPortal } from 'react-dom';
import { StickerPicker } from './StickerPicker';
import { countStickers } from './lib';
import type { StickerPackType, StickerType } from '../../state/ducks/stickers';
import type { LocalizerType } from '../../types/Util';
import type { Theme } from '../../util/theme';
import { StickerPicker } from './StickerPicker';
import { countStickers } from './lib';
import { offsetDistanceModifier } from '../../util/popperUtil';
import { themeClassName } from '../../util/theme';
export type OwnProps = {
readonly className?: string;
readonly i18n: LocalizerType;
readonly receivedPacks: ReadonlyArray<StickerPackType>;
readonly installedPacks: ReadonlyArray<StickerPackType>;
@ -21,19 +25,25 @@ export type OwnProps = {
readonly installedPack?: StickerPackType | null;
readonly recentStickers: ReadonlyArray<StickerType>;
readonly clearInstalledStickerPack: () => unknown;
readonly onClickAddPack: () => unknown;
readonly onPickSticker: (packId: string, stickerId: number) => unknown;
readonly onClickAddPack?: () => unknown;
readonly onPickSticker: (
packId: string,
stickerId: number,
url: string
) => unknown;
readonly showIntroduction?: boolean;
readonly clearShowIntroduction: () => unknown;
readonly showPickerHint: boolean;
readonly clearShowPickerHint: () => unknown;
readonly position?: 'top-end' | 'top-start';
readonly theme?: Theme;
};
export type Props = OwnProps;
export const StickerButton = React.memo(
({
className,
i18n,
clearInstalledStickerPack,
onClickAddPack,
@ -49,6 +59,7 @@ export const StickerButton = React.memo(
showPickerHint,
clearShowPickerHint,
position = 'top-end',
theme,
}: Props) => {
const [open, setOpen] = React.useState(false);
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(
@ -62,7 +73,7 @@ export const StickerButton = React.memo(
// Handle button click
if (installedPacks.length === 0) {
onClickAddPack();
onClickAddPack?.();
} else if (popperRoot) {
setOpen(false);
} else {
@ -78,9 +89,9 @@ export const StickerButton = React.memo(
]);
const handlePickSticker = React.useCallback(
(packId: string, stickerId: number) => {
(packId: string, stickerId: number, url: string) => {
setOpen(false);
onPickSticker(packId, stickerId);
onPickSticker(packId, stickerId, url);
},
[setOpen, onPickSticker]
);
@ -94,7 +105,7 @@ export const StickerButton = React.memo(
if (showPickerHint) {
clearShowPickerHint();
}
onClickAddPack();
onClickAddPack?.();
}, [onClickAddPack, showPickerHint, clearShowPickerHint]);
const handleClearIntroduction = React.useCallback(() => {
@ -110,13 +121,16 @@ export const StickerButton = React.memo(
document.body.appendChild(root);
const handleOutsideClick = ({ target }: MouseEvent) => {
const targetElement = target as HTMLElement;
const className = targetElement ? targetElement.className || '' : '';
const targetClassName = targetElement
? targetElement.className || ''
: '';
// We need to special-case sticker picker header buttons, because they can
// disappear after being clicked, which breaks the .contains() check below.
const isMissingButtonClass =
!className ||
className.indexOf('module-sticker-picker__header__button') < 0;
!targetClassName ||
targetClassName.indexOf('module-sticker-picker__header__button') <
0;
if (!root.contains(targetElement) && isMissingButtonClass) {
setOpen(false);
@ -194,10 +208,13 @@ export const StickerButton = React.memo(
type="button"
ref={ref}
onClick={handleClickButton}
className={classNames({
'module-sticker-button__button': true,
'module-sticker-button__button--active': open,
})}
className={classNames(
{
'module-sticker-button__button': true,
'module-sticker-button__button--active': open,
},
className
)}
aria-label={i18n('stickers--StickerPicker--Open')}
/>
)}
@ -209,84 +226,88 @@ export const StickerButton = React.memo(
modifiers={[offsetDistanceModifier(6)]}
>
{({ ref, style, placement, arrowProps }) => (
<button
type="button"
ref={ref}
style={style}
className="module-sticker-button__tooltip"
onClick={clearInstalledStickerPack}
>
{installedPack.cover ? (
<img
className="module-sticker-button__tooltip__image"
src={installedPack.cover.url}
alt={installedPack.title}
/>
) : (
<div className="module-sticker-button__tooltip__image-placeholder" />
)}
<span className="module-sticker-button__tooltip__text">
<span className="module-sticker-button__tooltip__text__title">
{installedPack.title}
</span>{' '}
installed
</span>
<div
ref={arrowProps.ref}
style={arrowProps.style}
className={classNames(
'module-sticker-button__tooltip__triangle',
`module-sticker-button__tooltip__triangle--${placement}`
<div className={theme ? themeClassName(theme) : undefined}>
<button
type="button"
ref={ref}
style={style}
className="module-sticker-button__tooltip"
onClick={clearInstalledStickerPack}
>
{installedPack.cover ? (
<img
className="module-sticker-button__tooltip__image"
src={installedPack.cover.url}
alt={installedPack.title}
/>
) : (
<div className="module-sticker-button__tooltip__image-placeholder" />
)}
/>
</button>
<span className="module-sticker-button__tooltip__text">
<span className="module-sticker-button__tooltip__text__title">
{installedPack.title}
</span>{' '}
installed
</span>
<div
ref={arrowProps.ref}
style={arrowProps.style}
className={classNames(
'module-sticker-button__tooltip__triangle',
`module-sticker-button__tooltip__triangle--${placement}`
)}
/>
</button>
</div>
)}
</Popper>
) : null}
{!open && showIntroduction ? (
<Popper placement={position} modifiers={[offsetDistanceModifier(6)]}>
{({ ref, style, placement, arrowProps }) => (
<button
type="button"
ref={ref}
style={style}
className={classNames(
'module-sticker-button__tooltip',
'module-sticker-button__tooltip--introduction'
)}
onClick={handleClearIntroduction}
>
<img
className="module-sticker-button__tooltip--introduction__image"
srcSet="images/sticker_splash@1x.png 1x, images/sticker_splash@2x.png 2x"
alt={i18n('stickers--StickerManager--Introduction--Image')}
/>
<div className="module-sticker-button__tooltip--introduction__meta">
<div className="module-sticker-button__tooltip--introduction__meta__title">
{i18n('stickers--StickerManager--Introduction--Title')}
</div>
<div className="module-sticker-button__tooltip--introduction__meta__subtitle">
{i18n('stickers--StickerManager--Introduction--Body')}
</div>
</div>
<div className="module-sticker-button__tooltip--introduction__close">
<button
type="button"
className="module-sticker-button__tooltip--introduction__close__button"
onClick={handleClearIntroduction}
aria-label={i18n('close')}
/>
</div>
<div
ref={arrowProps.ref}
style={arrowProps.style}
<div className={theme ? themeClassName(theme) : undefined}>
<button
type="button"
ref={ref}
style={style}
className={classNames(
'module-sticker-button__tooltip__triangle',
'module-sticker-button__tooltip__triangle--introduction',
`module-sticker-button__tooltip__triangle--${placement}`
'module-sticker-button__tooltip',
'module-sticker-button__tooltip--introduction'
)}
/>
</button>
onClick={handleClearIntroduction}
>
<img
className="module-sticker-button__tooltip--introduction__image"
srcSet="images/sticker_splash@1x.png 1x, images/sticker_splash@2x.png 2x"
alt={i18n('stickers--StickerManager--Introduction--Image')}
/>
<div className="module-sticker-button__tooltip--introduction__meta">
<div className="module-sticker-button__tooltip--introduction__meta__title">
{i18n('stickers--StickerManager--Introduction--Title')}
</div>
<div className="module-sticker-button__tooltip--introduction__meta__subtitle">
{i18n('stickers--StickerManager--Introduction--Body')}
</div>
</div>
<div className="module-sticker-button__tooltip--introduction__close">
<button
type="button"
className="module-sticker-button__tooltip--introduction__close__button"
onClick={handleClearIntroduction}
aria-label={i18n('close')}
/>
</div>
<div
ref={arrowProps.ref}
style={arrowProps.style}
className={classNames(
'module-sticker-button__tooltip__triangle',
'module-sticker-button__tooltip__triangle--introduction',
`module-sticker-button__tooltip__triangle--${placement}`
)}
/>
</button>
</div>
)}
</Popper>
) : null}
@ -294,17 +315,21 @@ export const StickerButton = React.memo(
? createPortal(
<Popper placement={position}>
{({ ref, style }) => (
<StickerPicker
ref={ref}
i18n={i18n}
style={style}
packs={installedPacks}
onClose={handleClose}
onClickAddPack={handleClickAddPack}
onPickSticker={handlePickSticker}
recentStickers={recentStickers}
showPickerHint={showPickerHint}
/>
<div className={theme ? themeClassName(theme) : undefined}>
<StickerPicker
ref={ref}
i18n={i18n}
style={style}
packs={installedPacks}
onClose={handleClose}
onClickAddPack={
onClickAddPack ? handleClickAddPack : undefined
}
onPickSticker={handlePickSticker}
recentStickers={recentStickers}
showPickerHint={showPickerHint}
/>
</div>
)}
</Popper>,
popperRoot

View File

@ -12,8 +12,12 @@ import type { LocalizerType } from '../../types/Util';
export type OwnProps = {
readonly i18n: LocalizerType;
readonly onClose: () => unknown;
readonly onClickAddPack: () => unknown;
readonly onPickSticker: (packId: string, stickerId: number) => unknown;
readonly onClickAddPack?: () => unknown;
readonly onPickSticker: (
packId: string,
stickerId: number,
url: string
) => unknown;
readonly packs: ReadonlyArray<StickerPackType>;
readonly recentStickers: ReadonlyArray<StickerType>;
readonly showPickerHint?: boolean;
@ -230,20 +234,22 @@ export const StickerPicker = React.memo(
/>
) : null}
</div>
<button
type="button"
ref={addPackRef}
className={classNames(
'module-sticker-picker__header__button',
'module-sticker-picker__header__button--add-pack',
{
'module-sticker-picker__header__button--hint':
showPickerHint,
}
)}
onClick={onClickAddPack}
aria-label={i18n('stickers--StickerPicker--AddPack')}
/>
{onClickAddPack && (
<button
type="button"
ref={addPackRef}
className={classNames(
'module-sticker-picker__header__button',
'module-sticker-picker__header__button--add-pack',
{
'module-sticker-picker__header__button--hint':
showPickerHint,
}
)}
onClick={onClickAddPack}
aria-label={i18n('stickers--StickerPicker--AddPack')}
/>
)}
</div>
<div
className={classNames('module-sticker-picker__body', {
@ -317,7 +323,7 @@ export const StickerPicker = React.memo(
ref={maybeFocusRef}
key={`${packId}-${id}`}
className="module-sticker-picker__body__cell"
onClick={() => onPickSticker(packId, id)}
onClick={() => onPickSticker(packId, id, url)}
>
<img
className="module-sticker-picker__body__cell__image"

21
ts/hooks/usePortal.ts Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useEffect, useState } from 'react';
export function usePortal(): HTMLDivElement | null {
const [root, setRoot] = useState<HTMLDivElement | null>(null);
useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div);
setRoot(div);
return () => {
document.body.removeChild(div);
setRoot(null);
};
}, []);
return root;
}

9
ts/hooks/useUniqueId.ts Normal file
View File

@ -0,0 +1,9 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useMemo } from 'react';
import { v4 as uuid } from 'uuid';
export function useUniqueId(): string {
return useMemo(() => uuid(), []);
}

View File

@ -0,0 +1,12 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export type ImageStateType = {
angle: number;
cropX: number;
cropY: number;
flipX: boolean;
flipY: boolean;
height: number;
width: number;
};

View File

@ -0,0 +1,196 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { fabric } from 'fabric';
import { clamp } from 'lodash';
export class MediaEditorFabricCropRect extends fabric.Rect {
static PADDING = 4;
constructor(options?: fabric.IRectOptions) {
super({
fill: undefined,
lockScalingFlip: true,
...(options || {}),
});
this.on('modified', this.containBounds.bind(this));
}
private containBounds() {
if (!this.canvas) {
return;
}
const zoom = this.canvas.getZoom() || 1;
const { left, top, height, width } = this.getBoundingRect();
const canvasHeight = this.canvas.getHeight();
const canvasWidth = this.canvas.getWidth();
if (height > canvasHeight || width > canvasWidth) {
this.canvas.discardActiveObject();
} else {
this.set(
'left',
clamp(
left / zoom,
MediaEditorFabricCropRect.PADDING / zoom,
(canvasWidth - width - MediaEditorFabricCropRect.PADDING) / zoom
)
);
this.set(
'top',
clamp(
top / zoom,
MediaEditorFabricCropRect.PADDING / zoom,
(canvasHeight - height - MediaEditorFabricCropRect.PADDING) / zoom
)
);
}
this.setCoords();
}
override render(ctx: CanvasRenderingContext2D): void {
super.render(ctx);
const bounds = this.getBoundingRect();
const zoom = this.canvas?.getZoom() || 1;
const canvasWidth = (this.canvas?.getWidth() || 0) / zoom;
const canvasHeight = (this.canvas?.getHeight() || 0) / zoom;
const height = bounds.height / zoom;
const left = bounds.left / zoom;
const top = bounds.top / zoom;
const width = bounds.width / zoom;
ctx.save();
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
// top
ctx.fillRect(0, 0, canvasWidth, top);
// left
ctx.fillRect(0, top, left, height);
// bottom
ctx.fillRect(0, height + top, canvasWidth, canvasHeight - top);
// right
ctx.fillRect(left + width, top, canvasWidth - left, height);
ctx.restore();
}
}
MediaEditorFabricCropRect.prototype.controls = {
tl: new fabric.Control({
x: -0.5,
y: -0.5,
actionHandler: fabric.controlsUtils.scalingEqually,
render: (
ctx: CanvasRenderingContext2D,
left: number,
top: number,
_,
rect: fabric.Object
) => {
const WIDTH = getMinSize(rect.width);
ctx.save();
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(left - 2, top + WIDTH);
ctx.lineTo(left - 2, top - 2);
ctx.lineTo(left + WIDTH, top - 2);
ctx.stroke();
ctx.restore();
},
}),
tr: new fabric.Control({
x: 0.5,
y: -0.5,
actionHandler: fabric.controlsUtils.scalingEqually,
render: (
ctx: CanvasRenderingContext2D,
left: number,
top: number,
_,
rect: fabric.Object
) => {
const WIDTH = getMinSize(rect.width);
ctx.save();
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(left + 2, top + WIDTH);
ctx.lineTo(left + 2, top - 2);
ctx.lineTo(left - WIDTH, top - 2);
ctx.stroke();
ctx.restore();
},
}),
bl: new fabric.Control({
x: -0.5,
y: 0.5,
actionHandler: fabric.controlsUtils.scalingEqually,
render: (
ctx: CanvasRenderingContext2D,
left: number,
top: number,
_,
rect: fabric.Object
) => {
const WIDTH = getMinSize(rect.width);
ctx.save();
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(left - 2, top - WIDTH);
ctx.lineTo(left - 2, top + 2);
ctx.lineTo(left + WIDTH, top + 2);
ctx.stroke();
ctx.restore();
},
}),
br: new fabric.Control({
x: 0.5,
y: 0.5,
actionHandler: fabric.controlsUtils.scalingEqually,
render: (
ctx: CanvasRenderingContext2D,
left: number,
top: number,
_,
rect: fabric.Object
) => {
const WIDTH = getMinSize(rect.width);
ctx.save();
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(left + 2, top - WIDTH);
ctx.lineTo(left + 2, top + 2);
ctx.lineTo(left - WIDTH, top + 2);
ctx.stroke();
ctx.restore();
},
}),
};
MediaEditorFabricCropRect.prototype.excludeFromExport = true;
MediaEditorFabricCropRect.prototype.borderColor = '#ffffff';
MediaEditorFabricCropRect.prototype.cornerColor = '#ffffff';
function getMinSize(width: number | undefined): number {
return Math.min(width || 24, 24);
}

View File

@ -0,0 +1,35 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { fabric } from 'fabric';
import { customFabricObjectControls } from './util/customFabricObjectControls';
export class MediaEditorFabricIText extends fabric.IText {
constructor(text: string, options: fabric.ITextOptions) {
super(text, {
fontFamily: 'Inter',
fontWeight: 'bold',
lockScalingFlip: true,
originX: 'center',
originY: 'center',
textAlign: 'center',
...options,
});
}
static override fromObject(
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
options: any,
callback: (_: MediaEditorFabricIText) => unknown
): MediaEditorFabricIText {
const result = new MediaEditorFabricIText(options.text, options);
callback(result);
return result;
}
}
MediaEditorFabricIText.prototype.type = 'MediaEditorFabricIText';
MediaEditorFabricIText.prototype.lockScalingFlip = true;
MediaEditorFabricIText.prototype.borderColor = '#ffffff';
MediaEditorFabricIText.prototype.controls = customFabricObjectControls;

View File

@ -0,0 +1,29 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { fabric } from 'fabric';
import { customFabricObjectControls } from './util/customFabricObjectControls';
export class MediaEditorFabricPath extends fabric.Path {
constructor(
path?: string | Array<fabric.Point>,
options?: fabric.IPathOptions
) {
super(path, { fill: undefined, lockScalingFlip: true, ...(options || {}) });
}
static override fromObject(
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
options: any,
callback: (_: MediaEditorFabricPath) => unknown
): MediaEditorFabricPath {
const result = new MediaEditorFabricPath(options.path, options);
callback(result);
return result;
}
}
MediaEditorFabricPath.prototype.type = 'MediaEditorFabricPath';
MediaEditorFabricPath.prototype.borderColor = '#ffffff';
MediaEditorFabricPath.prototype.controls = customFabricObjectControls;

View File

@ -0,0 +1,23 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { fabric } from 'fabric';
import { MediaEditorFabricPath } from './MediaEditorFabricPath';
export class MediaEditorFabricPencilBrush extends fabric.PencilBrush {
public strokeMiterLimit: undefined | number;
override createPath(
pathData?: string | Array<fabric.Point>
): MediaEditorFabricPath {
return new MediaEditorFabricPath(pathData, {
fill: undefined,
stroke: this.color,
strokeWidth: this.width,
strokeLineCap: this.strokeLineCap,
strokeMiterLimit: this.strokeMiterLimit,
strokeLineJoin: this.strokeLineJoin,
strokeDashArray: this.strokeDashArray,
});
}
}

View File

@ -0,0 +1,36 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { fabric } from 'fabric';
import { customFabricObjectControls } from './util/customFabricObjectControls';
export class MediaEditorFabricSticker extends fabric.Image {
constructor(
element: string | HTMLImageElement | HTMLVideoElement,
options: fabric.IImageOptions = {}
) {
// Fabric seems to have issues when passed a string, but not an Image.
let normalizedElement: undefined | HTMLImageElement | HTMLVideoElement;
if (typeof element === 'string') {
normalizedElement = new Image();
normalizedElement.src = element;
} else {
normalizedElement = element;
}
super(normalizedElement, options);
}
static fromObject(
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
options: any,
callback: (_: MediaEditorFabricSticker) => unknown
): void {
callback(new MediaEditorFabricSticker(options.src, options));
}
}
MediaEditorFabricSticker.prototype.type = 'MediaEditorFabricSticker';
MediaEditorFabricSticker.prototype.borderColor = '#ffffff';
MediaEditorFabricSticker.prototype.controls = customFabricObjectControls;

View File

@ -0,0 +1,152 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useEffect, useState } from 'react';
import { fabric } from 'fabric';
import EventEmitter from 'events';
import type { ImageStateType } from './ImageStateType';
import { MediaEditorFabricIText } from './MediaEditorFabricIText';
import { MediaEditorFabricPath } from './MediaEditorFabricPath';
import { MediaEditorFabricSticker } from './MediaEditorFabricSticker';
export function useFabricHistory(
canvas: fabric.Canvas | undefined
): FabricHistory | undefined {
const [history, setHistory] = useState<FabricHistory | undefined>();
// We need this type of precision so that when serializing/deserializing
// the floats don't get rounded off and we maintain proper image state.
// http://fabricjs.com/fabric-gotchas
fabric.Object.NUM_FRACTION_DIGITS = 16;
// Attach our custom classes to the global Fabric instance. Unfortunately, Fabric
// doesn't make it easy to deserialize into a custom class without polluting the
// global namespace. See <http://fabricjs.com/fabric-intro-part-3#subclassing>.
Object.assign(fabric, {
MediaEditorFabricIText,
MediaEditorFabricPath,
MediaEditorFabricSticker,
});
useEffect(() => {
if (canvas) {
const fabricHistory = new FabricHistory(canvas);
setHistory(fabricHistory);
}
}, [canvas]);
return history;
}
const LIMIT = 1000;
type SnapshotStateType = {
canvasState: string;
imageState?: ImageStateType;
};
export class FabricHistory extends EventEmitter {
private readonly canvas: fabric.Canvas;
private highWatermark: number;
private isTimeTraveling: boolean;
private snapshots: Array<SnapshotStateType>;
constructor(canvas: fabric.Canvas) {
super();
this.canvas = canvas;
this.highWatermark = 0;
this.isTimeTraveling = false;
this.snapshots = [];
this.canvas.on('object:added', this.onObjectModified.bind(this));
this.canvas.on('object:modified', this.onObjectModified.bind(this));
this.canvas.on('object:removed', this.onObjectModified.bind(this));
}
private applyState({ canvasState, imageState }: SnapshotStateType): void {
this.canvas.loadFromJSON(canvasState, () => {
this.emit('appliedState', imageState);
this.emit('historyChanged');
this.isTimeTraveling = false;
});
}
private getState(): string {
return JSON.stringify(this.canvas.toDatalessJSON());
}
private onObjectModified({ target }: fabric.IEvent): void {
if (target?.excludeFromExport) {
return;
}
this.takeSnapshot();
}
private getUndoState(): SnapshotStateType | undefined {
if (!this.canUndo()) {
return;
}
this.highWatermark -= 1;
return this.snapshots[this.highWatermark];
}
private getRedoState(): SnapshotStateType | undefined {
if (this.canRedo()) {
this.highWatermark += 1;
}
return this.snapshots[this.highWatermark];
}
public takeSnapshot(imageState?: ImageStateType): void {
if (this.isTimeTraveling) {
return;
}
if (this.canRedo()) {
this.snapshots.splice(this.highWatermark, this.snapshots.length);
}
this.snapshots.push({ canvasState: this.getState(), imageState });
if (this.snapshots.length > LIMIT) {
this.snapshots.shift();
}
this.highWatermark = this.snapshots.length - 1;
this.emit('historyChanged');
}
public undo(): void {
const undoState = this.getUndoState();
if (!undoState) {
return;
}
this.isTimeTraveling = true;
this.applyState(undoState);
}
public redo(): void {
const redoState = this.getRedoState();
if (!redoState) {
return;
}
this.isTimeTraveling = true;
this.applyState(redoState);
}
public canUndo(): boolean {
return this.highWatermark > 0;
}
public canRedo(): boolean {
return this.highWatermark < this.snapshots.length - 1;
}
}

View File

@ -0,0 +1,47 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
function getRatio(min: number, max: number, value: number) {
return (value - min) / (max - min);
}
function getHSLValues(percentage: number): [number, number, number] {
if (percentage <= 10) {
return [0, 0, 1 - getRatio(0, 10, percentage)];
}
if (percentage < 20) {
return [0, 0.5, 0.5 * getRatio(10, 20, percentage)];
}
const ratio = getRatio(20, 100, percentage);
return [360 * ratio, 1, 0.5];
}
export function getHSL(percentage: number): string {
const [h, s, l] = getHSLValues(percentage);
return `hsl(${h}, ${s * 100}%, ${l * 100}%)`;
}
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative
export function getRGBA(percentage: number, alpha = 1): string {
const [h, s, l] = getHSLValues(percentage);
const a = s * Math.min(l, 1 - l);
function f(n: number): number {
const k = (n + h / 30) % 12;
return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
}
const rgbValue = [
Math.round(255 * f(0)),
Math.round(255 * f(8)),
Math.round(255 * f(4)),
]
.map(String)
.join(',');
return `rgba(${rgbValue},${alpha})`;
}

View File

@ -0,0 +1,134 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { fabric } from 'fabric';
const resizeControl = new fabric.Control({
actionHandler: fabric.controlsUtils.scalingEqually,
cursorStyleHandler: () => 'se-resize',
render: (ctx: CanvasRenderingContext2D, left: number, top: number) => {
// circle
const size = 9;
ctx.save();
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(left, top, size, 0, 2 * Math.PI, false);
ctx.fill();
// arrows NW & SE
const arrowSize = 4;
ctx.fillStyle = '#3b3b3b';
ctx.strokeStyle = '#3b3b3b';
ctx.beginPath();
// SE
ctx.moveTo(left + 0.5, top + 0.5);
ctx.lineTo(left + arrowSize, top + arrowSize);
ctx.moveTo(left + arrowSize, top + 1);
ctx.lineTo(left + arrowSize, top + arrowSize);
ctx.lineTo(left + 1, top + arrowSize);
// NW
ctx.moveTo(left - 0.5, top - 0.5);
ctx.lineTo(left - arrowSize, top - arrowSize);
ctx.moveTo(left - arrowSize, top - 1);
ctx.lineTo(left - arrowSize, top - arrowSize);
ctx.lineTo(left - 1, top - arrowSize);
ctx.stroke();
ctx.restore();
},
x: 0.5,
y: 0.5,
});
const rotateControl = new fabric.Control({
actionHandler: fabric.controlsUtils.rotationWithSnapping,
actionName: 'rotate',
cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
offsetY: -40,
render(
ctx: CanvasRenderingContext2D,
left: number,
top: number,
_,
target: fabric.Object
) {
const size = 5;
ctx.save();
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#fff';
// connecting line
ctx.beginPath();
ctx.moveTo(left, top);
const radians = 0 - ((target.angle || 0) * Math.PI) / 180;
const targetLeft = 40 * Math.sin(radians);
const targetTop = 40 * Math.cos(radians);
ctx.lineTo(left + targetLeft, top + targetTop);
ctx.stroke();
// circle
ctx.beginPath();
ctx.moveTo(left, top);
ctx.arc(left, top, size, 0, 2 * Math.PI, false);
ctx.fill();
ctx.restore();
},
withConnection: false,
x: 0,
y: -0.5,
});
const deleteControl = new fabric.Control({
cursorStyleHandler: () => 'pointer',
// This is lifted from <http://fabricjs.com/custom-control-render>.
mouseUpHandler: (_eventData, { target }) => {
if (!target.canvas) {
return false;
}
target.canvas.remove(target);
return true;
},
render: (ctx: CanvasRenderingContext2D, left: number, top: number) => {
// circle
const size = 9;
ctx.save();
ctx.fillStyle = '#000';
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(left, top, size, 0, 2 * Math.PI, false);
ctx.fill();
// x
const xSize = 3;
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#fff';
ctx.beginPath();
const topLeft = new fabric.Point(left - xSize, top - xSize);
const topRight = new fabric.Point(left + xSize, top - xSize);
const bottomRight = new fabric.Point(left + xSize, top + xSize);
const bottomLeft = new fabric.Point(left - xSize, top + xSize);
ctx.moveTo(topLeft.x, topLeft.y);
ctx.lineTo(bottomRight.x, bottomRight.y);
ctx.moveTo(topRight.x, topRight.y);
ctx.lineTo(bottomLeft.x, bottomLeft.y);
ctx.stroke();
ctx.restore();
},
x: -0.5,
y: -0.5,
});
export const customFabricObjectControls = {
br: resizeControl,
mtr: rotateControl,
tl: deleteControl,
};

View File

@ -0,0 +1,44 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../../logging/log';
import { getHSL } from './color';
import { missingCaseError } from '../../util/missingCaseError';
export enum TextStyle {
Regular = 'Regular',
Highlight = 'Highlight',
Outline = 'Outline',
}
export function getTextStyleAttributes(
textStyle: TextStyle,
hueSliderValue: number
): {
fill: string;
stroke?: string;
strokeWidth: number;
textBackgroundColor: string;
} {
const color = getHSL(hueSliderValue);
switch (textStyle) {
case TextStyle.Regular:
return { fill: color, strokeWidth: 0, textBackgroundColor: '' };
case TextStyle.Highlight:
return {
fill: hueSliderValue <= 5 ? '#000' : '#fff',
strokeWidth: 0,
textBackgroundColor: color,
};
case TextStyle.Outline:
return {
fill: hueSliderValue <= 5 ? '#000' : '#fff',
stroke: color,
strokeWidth: 2,
textBackgroundColor: '',
};
default:
log.error(missingCaseError(textStyle));
return getTextStyleAttributes(TextStyle.Regular, hueSliderValue);
}
}

View File

@ -123,9 +123,10 @@ function addAttachment(
? getState().composer.attachments
: getAttachmentsFromConversationModel(conversationId);
// We expect there to either be a pending draft attachment or an existing
// attachment that we'll be replacing.
const hasDraftAttachmentPending = draftAttachments.some(
draftAttachment =>
draftAttachment.pending && draftAttachment.path === attachment.path
draftAttachment => draftAttachment.path === attachment.path
);
// User has canceled the draft so we don't need to continue processing

View File

@ -0,0 +1,96 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { StickerPackType, StickerType } from '../../state/ducks/stickers';
export const createPack = (
props: Partial<StickerPackType>,
sticker?: StickerType
): StickerPackType => ({
id: '',
title: props.id ? `${props.id} title` : 'title',
key: '',
author: '',
isBlessed: false,
lastUsed: 0,
status: 'known',
cover: sticker,
stickerCount: 101,
stickers: sticker
? Array(101)
.fill(0)
.map((_, id) => ({ ...sticker, id }))
: [],
...props,
});
export const Stickers: Record<string, StickerType> = {
kitten1: {
id: 1,
url: '/fixtures/kitten-1-64-64.jpg',
packId: 'kitten1',
emoji: '',
},
kitten2: {
id: 2,
url: '/fixtures/kitten-2-64-64.jpg',
packId: 'kitten2',
emoji: '',
},
kitten3: {
id: 3,
url: '/fixtures/kitten-3-64-64.jpg',
packId: 'kitten3',
emoji: '',
},
abe: {
id: 4,
url: '/fixtures/512x515-thumbs-up-lincoln.webp',
packId: 'abe',
emoji: '',
},
wide: {
id: 5,
url: '/fixtures/1000x50-green.jpeg',
packId: 'wide',
emoji: '',
},
tall: {
id: 6,
url: '/fixtures/50x1000-teal.jpeg',
packId: 'tall',
emoji: '',
},
};
export const receivedPacks = [
createPack({ id: 'abe', status: 'downloaded' }, Stickers.abe),
createPack({ id: 'kitten3', status: 'downloaded' }, Stickers.kitten3),
];
export const installedPacks = [
createPack({ id: 'kitten1', status: 'installed' }, Stickers.kitten1),
createPack({ id: 'kitten2', status: 'installed' }, Stickers.kitten2),
createPack({ id: 'kitten3', status: 'installed' }, Stickers.kitten3),
];
export const blessedPacks = [
createPack(
{ id: 'wide', status: 'downloaded', isBlessed: true },
Stickers.wide
),
createPack(
{ id: 'tall', status: 'downloaded', isBlessed: true },
Stickers.tall
),
];
export const knownPacks = [
createPack({ id: 'kitten1', status: 'known' }, Stickers.kitten1),
createPack({ id: 'kitten2', status: 'known' }, Stickers.kitten2),
];

17
ts/util/canEditImages.ts Normal file
View File

@ -0,0 +1,17 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isEnabled } from '../RemoteConfig';
import { getEnvironment, Environment } from '../environment';
import { isBeta } from './version';
export function canEditImages(): boolean {
return (
isEnabled('desktop.internalUser') ||
getEnvironment() === Environment.Staging ||
getEnvironment() === Environment.Development ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Boolean((window as any).STORYBOOK_ENV) ||
isBeta(window.getVersion())
);
}

File diff suppressed because it is too large Load Diff

324
yarn.lock
View File

@ -1705,6 +1705,21 @@
lodash "^4.17.15"
tmp-promise "^3.0.2"
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950"
integrity sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==
dependencies:
detect-libc "^1.0.3"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.1"
nopt "^5.0.0"
npmlog "^4.1.2"
rimraf "^3.0.2"
semver "^7.3.4"
tar "^6.1.0"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@ -2499,6 +2514,11 @@
"@types/express-serve-static-core" "*"
"@types/serve-static" "*"
"@types/fabric@4.5.3":
version "4.5.3"
resolved "https://registry.yarnpkg.com/@types/fabric/-/fabric-4.5.3.tgz#f4f2e1168d086a7ffe12e5cea4193d0cd6a526f7"
integrity sha512-DCneYSkuVdGYpFbDQ2j5zT7DDdAiOlAPfSjS3PsVWHFt6f/DapCdV0ansPq3Ai5oe+j6BgFhdkh+DWne1yQMdw==
"@types/filesize@3.6.0":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@types/filesize/-/filesize-3.6.0.tgz#5f1a25c7b4e3d5ee2bc63133d374d096b7008c8d"
@ -3471,6 +3491,11 @@
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
abab@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
abbrev@1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
@ -3497,16 +3522,39 @@ accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"
acorn-globals@^4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
dependencies:
acorn "^6.0.1"
acorn-walk "^6.0.1"
acorn-jsx@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe"
integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==
acorn-walk@^6.0.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
acorn@^6.0.1:
version "6.4.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
acorn@^6.2.1:
version "6.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
acorn@^7.1.0:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^7.4.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c"
@ -3871,6 +3919,11 @@ array-each@^1.0.1:
resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f"
integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8=
array-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
array-filter@~0.0.0:
version "0.0.1"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
@ -4870,6 +4923,11 @@ brorand@^1.0.1, brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browser-process-hrtime@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
browser-stdout@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
@ -5228,6 +5286,15 @@ caniuse-lite@^1.0.30001181:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001207.tgz#364d47d35a3007e528f69adb6fecb07c2bb2cc50"
integrity sha512-UPQZdmAsyp2qfCTiMU/zqGSWOYaY9F9LL61V8f+8MrubsaDGpaHD9HRV/EWZGULZn0Hxu48SKzI5DgFwTvHuYw==
canvas@^2.6.1:
version "2.8.0"
resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.8.0.tgz#f99ca7f25e6e26686661ffa4fec1239bbef74461"
integrity sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
nan "^2.14.0"
simple-get "^3.0.3"
case-sensitive-paths-webpack-plugin@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.2.0.tgz#3371ef6365ef9c25fa4b81c16ace0e9c7dc58c3e"
@ -6307,6 +6374,23 @@ csso@^3.5.1:
dependencies:
css-tree "1.0.0-alpha.29"
cssom@^0.4.1:
version "0.4.4"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
cssom@~0.3.6:
version "0.3.8"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
cssstyle@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
dependencies:
cssom "~0.3.6"
csstype@^2.2.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01"
@ -6351,6 +6435,15 @@ data-uri-to-buffer@3:
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636"
integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==
data-urls@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
dependencies:
abab "^2.0.0"
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -6805,6 +6898,13 @@ domelementtype@^2.0.1:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
dependencies:
webidl-conversions "^4.0.2"
domhandler@^2.3.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
@ -7386,7 +7486,7 @@ escape-string-regexp@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
escodegen@^1.8.1:
escodegen@^1.11.1, escodegen@^1.8.1:
version "1.14.3"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
@ -7972,6 +8072,14 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fabric@4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/fabric/-/fabric-4.6.0.tgz#bd11c2baf165db2c97e4d05740d931586cb26bbb"
integrity sha512-MhJXCD/ZugOGV5aPHIG0MY1q2EfrlzC2sasrAHj0HHXN50JTe1bHFrlRdkXBijCJ0dG81fGu/A/Pct9DyuwCzQ==
optionalDependencies:
canvas "^2.6.1"
jsdom "^15.2.1"
fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
@ -9569,6 +9677,13 @@ hpack.js@^2.1.6:
readable-stream "^2.0.1"
wbuf "^1.1.0"
html-encoding-sniffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
dependencies:
whatwg-encoding "^1.0.1"
html-entities@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
@ -10767,6 +10882,38 @@ jsbn@~0.1.0:
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
jsdom@^15.2.1:
version "15.2.1"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5"
integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==
dependencies:
abab "^2.0.0"
acorn "^7.1.0"
acorn-globals "^4.3.2"
array-equal "^1.0.0"
cssom "^0.4.1"
cssstyle "^2.0.0"
data-urls "^1.1.0"
domexception "^1.0.1"
escodegen "^1.11.1"
html-encoding-sniffer "^1.0.2"
nwsapi "^2.2.0"
parse5 "5.1.0"
pn "^1.1.0"
request "^2.88.0"
request-promise-native "^1.0.7"
saxes "^3.1.9"
symbol-tree "^3.2.2"
tough-cookie "^3.0.1"
w3c-hr-time "^1.0.1"
w3c-xmlserializer "^1.1.2"
webidl-conversions "^4.0.2"
whatwg-encoding "^1.0.5"
whatwg-mimetype "^2.3.0"
whatwg-url "^7.0.0"
ws "^7.0.0"
xml-name-validator "^3.0.0"
jsesc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
@ -11206,6 +11353,11 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
@ -11359,6 +11511,13 @@ make-dir@^3.0.0:
dependencies:
semver "^6.0.0"
make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
dependencies:
semver "^6.0.0"
make-error@^1.1.1:
version "1.3.5"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
@ -12050,6 +12209,11 @@ nan@^2.12.1, nan@^2.13.2:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
nan@^2.14.0:
version "2.15.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
nanoid@3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
@ -12186,6 +12350,13 @@ node-fetch@^1.0.1:
encoding "^0.1.11"
is-stream "^1.0.1"
node-fetch@^2.6.1:
version "2.6.5"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
dependencies:
whatwg-url "^5.0.0"
node-forge@0.10.0, node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
@ -12496,6 +12667,11 @@ number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
nyc@11.4.1:
version "11.4.1"
resolved "https://registry.yarnpkg.com/nyc/-/nyc-11.4.1.tgz#13fdf7e7ef22d027c61d174758f6978a68f4f5e5"
@ -13099,6 +13275,11 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
parse5@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
parseurl@~1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
@ -13392,6 +13573,11 @@ plist@^3.0.1:
xmlbuilder "^9.0.7"
xmldom "^0.5.0"
pn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
pngjs@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
@ -14957,6 +15143,22 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
request-promise-core@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
dependencies:
lodash "^4.17.19"
request-promise-native@^1.0.7:
version "1.0.9"
resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
dependencies:
request-promise-core "1.1.4"
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
request@^2.45.0:
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
@ -15368,6 +15570,13 @@ sax@^1.2.4, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
saxes@^3.1.9:
version "3.1.11"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b"
integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==
dependencies:
xmlchars "^2.1.1"
scheduler@^0.13.3:
version "0.13.3"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896"
@ -16149,6 +16358,11 @@ stdout-stream@^1.4.0:
dependencies:
readable-stream "^2.0.1"
stealthy-require@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
store2@^2.7.1:
version "2.8.0"
resolved "https://registry.yarnpkg.com/store2/-/store2-2.8.0.tgz#032d5dcbd185a5d74049d67a1765ff1e75faa04b"
@ -16513,6 +16727,11 @@ symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
symbol-tree@^3.2.2:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
symbol.prototype.description@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/symbol.prototype.description/-/symbol.prototype.description-1.0.0.tgz#6e355660eb1e44ca8ad53a68fdb72ef131ca4b12"
@ -16877,6 +17096,23 @@ touch@^2.0.1:
dependencies:
nopt "~1.0.10"
tough-cookie@^2.3.3, tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
dependencies:
ip-regex "^2.1.0"
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@~2.3.0:
version "2.3.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
@ -16891,13 +17127,17 @@ tough-cookie@~2.4.3:
psl "^1.1.24"
punycode "^1.4.1"
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
punycode "^2.1.0"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
trim-newlines@^1.0.0:
version "1.0.0"
@ -17450,6 +17690,22 @@ vm2@^3.9.3:
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.5.tgz#5288044860b4bbace443101fcd3bddb2a0aa2496"
integrity sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng==
w3c-hr-time@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
dependencies:
browser-process-hrtime "^1.0.0"
w3c-xmlserializer@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794"
integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==
dependencies:
domexception "^1.0.1"
webidl-conversions "^4.0.2"
xml-name-validator "^3.0.0"
warning@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
@ -17518,6 +17774,16 @@ webdriverio@^4.13.0:
wdio-dot-reporter "~0.0.8"
wgxpath "~1.0.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
webpack-cli@4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.6.0.tgz#27ae86bfaec0cf393fcfd58abdc5a229ad32fd16"
@ -17742,10 +18008,39 @@ wgxpath@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wgxpath/-/wgxpath-1.0.0.tgz#eef8a4b9d558cc495ad3a9a2b751597ecd9af690"
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
dependencies:
iconv-lite "0.4.24"
whatwg-fetch@>=0.10.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
@ -17905,6 +18200,11 @@ ws@^6.2.1:
dependencies:
async-limiter "~1.0.0"
ws@^7.0.0:
version "7.5.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
ws@^7.3.1:
version "7.4.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
@ -17915,6 +18215,11 @@ xdg-basedir@^4.0.0:
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
xmlbuilder@>=11.0.1:
version "15.1.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"
@ -17925,6 +18230,11 @@ xmlbuilder@^9.0.7:
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
xmlchars@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmldom@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e"