Add image editor
|
@ -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)
|
||||
|
|
|
@ -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
After Width: | Height: | Size: 242 KiB |
1
images/icons/v2/crop-24.svg
Normal 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 |
1
images/icons/v2/crop-lock-24.svg
Normal 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 |
1
images/icons/v2/crop-unlock-24.svg
Normal 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 |
1
images/icons/v2/draw-24.svg
Normal 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 |
1
images/icons/v2/flip-outline-24.svg
Normal 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 |
1
images/icons/v2/pen-20.svg
Normal 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 |
1
images/icons/v2/pen-heavy-20.svg
Normal 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 |
1
images/icons/v2/pen-highlighter-20.svg
Normal 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 |
1
images/icons/v2/pen-light-20.svg
Normal 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 |
1
images/icons/v2/pen-medium-20.svg
Normal 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 |
1
images/icons/v2/pen-regular-20.svg
Normal 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 |
1
images/icons/v2/redo-24.svg
Normal 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 |
1
images/icons/v2/rotate-outline-24.svg
Normal 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 |
1
images/icons/v2/sticker-smiley-24.svg
Normal 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 |
|
@ -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 |
1
images/icons/v2/text-highlight-20.svg
Normal 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 |
1
images/icons/v2/text-outline-20.svg
Normal 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 |
1
images/icons/v2/text-regular-20.svg
Normal 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 |
|
@ -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 |
|
@ -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",
|
||||
|
|
129
patches/@types+fabric+4.5.3.patch
Normal 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
|
22
patches/fabric+4.6.0.patch
Normal 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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
116
stylesheets/components/ContextMenu.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
340
stylesheets/components/MediaEditor.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
39
ts/components/ContextMenu.stories.tsx
Normal 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()} />;
|
||||
});
|
186
ts/components/ContextMenu.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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: '',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
|
46
ts/components/MediaEditor.stories.tsx
Normal 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} />
|
||||
));
|
934
ts/components/MediaEditor.tsx
Normal 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
|
||||
);
|
||||
};
|
|
@ -43,7 +43,7 @@ story.add('One File', () => {
|
|||
}),
|
||||
],
|
||||
});
|
||||
return <AttachmentList {...props} />;
|
||||
return <AttachmentList {...props} canEditImages />;
|
||||
});
|
||||
|
||||
story.add('Multiple Visual Attachments', () => {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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(), []);
|
||||
}
|
12
ts/mediaEditor/ImageStateType.ts
Normal 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;
|
||||
};
|
196
ts/mediaEditor/MediaEditorFabricCropRect.ts
Normal 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);
|
||||
}
|
35
ts/mediaEditor/MediaEditorFabricIText.ts
Normal 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;
|
29
ts/mediaEditor/MediaEditorFabricPath.ts
Normal 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;
|
23
ts/mediaEditor/MediaEditorFabricPencilBrush.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
36
ts/mediaEditor/MediaEditorFabricSticker.ts
Normal 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;
|
152
ts/mediaEditor/useFabricHistory.ts
Normal 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;
|
||||
}
|
||||
}
|
47
ts/mediaEditor/util/color.ts
Normal 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})`;
|
||||
}
|
134
ts/mediaEditor/util/customFabricObjectControls.ts
Normal 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,
|
||||
};
|
44
ts/mediaEditor/util/getTextStyleAttributes.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
96
ts/test-both/helpers/getStickerPacks.ts
Normal 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
|
@ -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())
|
||||
);
|
||||
}
|
324
yarn.lock
|
@ -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"
|
||||
|
|