diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index a8f3a13df..109014021 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -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) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 69e42e0aa..0a9cc428e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/fixtures/snow.jpg b/fixtures/snow.jpg new file mode 100644 index 000000000..fe959acba Binary files /dev/null and b/fixtures/snow.jpg differ diff --git a/images/icons/v2/crop-24.svg b/images/icons/v2/crop-24.svg new file mode 100644 index 000000000..ecf4a007c --- /dev/null +++ b/images/icons/v2/crop-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/crop-lock-24.svg b/images/icons/v2/crop-lock-24.svg new file mode 100644 index 000000000..b0502a105 --- /dev/null +++ b/images/icons/v2/crop-lock-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/crop-unlock-24.svg b/images/icons/v2/crop-unlock-24.svg new file mode 100644 index 000000000..6c08707f5 --- /dev/null +++ b/images/icons/v2/crop-unlock-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/draw-24.svg b/images/icons/v2/draw-24.svg new file mode 100644 index 000000000..a6e79b10b --- /dev/null +++ b/images/icons/v2/draw-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/flip-outline-24.svg b/images/icons/v2/flip-outline-24.svg new file mode 100644 index 000000000..d8b7f2bcd --- /dev/null +++ b/images/icons/v2/flip-outline-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/pen-20.svg b/images/icons/v2/pen-20.svg new file mode 100644 index 000000000..0a184fd4a --- /dev/null +++ b/images/icons/v2/pen-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/pen-heavy-20.svg b/images/icons/v2/pen-heavy-20.svg new file mode 100644 index 000000000..f1bb57dda --- /dev/null +++ b/images/icons/v2/pen-heavy-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/pen-highlighter-20.svg b/images/icons/v2/pen-highlighter-20.svg new file mode 100644 index 000000000..646605821 --- /dev/null +++ b/images/icons/v2/pen-highlighter-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/pen-light-20.svg b/images/icons/v2/pen-light-20.svg new file mode 100644 index 000000000..e103b3c25 --- /dev/null +++ b/images/icons/v2/pen-light-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/pen-medium-20.svg b/images/icons/v2/pen-medium-20.svg new file mode 100644 index 000000000..ab289df4c --- /dev/null +++ b/images/icons/v2/pen-medium-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/pen-regular-20.svg b/images/icons/v2/pen-regular-20.svg new file mode 100644 index 000000000..e75ed1583 --- /dev/null +++ b/images/icons/v2/pen-regular-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/redo-24.svg b/images/icons/v2/redo-24.svg new file mode 100644 index 000000000..fd0cc572e --- /dev/null +++ b/images/icons/v2/redo-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/rotate-outline-24.svg b/images/icons/v2/rotate-outline-24.svg new file mode 100644 index 000000000..e455cc0ce --- /dev/null +++ b/images/icons/v2/rotate-outline-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/sticker-smiley-24.svg b/images/icons/v2/sticker-smiley-24.svg new file mode 100644 index 000000000..5186bee37 --- /dev/null +++ b/images/icons/v2/sticker-smiley-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/text-24.svg b/images/icons/v2/text-24.svg index 0e5e09922..25900fdae 100644 --- a/images/icons/v2/text-24.svg +++ b/images/icons/v2/text-24.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/images/icons/v2/text-highlight-20.svg b/images/icons/v2/text-highlight-20.svg new file mode 100644 index 000000000..b1012a114 --- /dev/null +++ b/images/icons/v2/text-highlight-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/text-outline-20.svg b/images/icons/v2/text-outline-20.svg new file mode 100644 index 000000000..4ab50f8ce --- /dev/null +++ b/images/icons/v2/text-outline-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/text-regular-20.svg b/images/icons/v2/text-regular-20.svg new file mode 100644 index 000000000..dfc8f9ba1 --- /dev/null +++ b/images/icons/v2/text-regular-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/undo-24.svg b/images/icons/v2/undo-24.svg index 2369b9d41..0deca433d 100644 --- a/images/icons/v2/undo-24.svg +++ b/images/icons/v2/undo-24.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/package.json b/package.json index 4eaedaa04..1701ad770 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/patches/@types+fabric+4.5.3.patch b/patches/@types+fabric+4.5.3.patch new file mode 100644 index 000000000..a5cb755db --- /dev/null +++ b/patches/@types+fabric+4.5.3.patch @@ -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, + 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 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; ++ + 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(actionHandler: ((eventData: Event, transform: Transform, x: number, y: number) => T)): ((eventData: Event, transform: Transform, x: number, y: number) => T); ++ wrapWithFireEvent(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; ++ + /** + * reset an object transform state to neutral. Top and left are not accounted for + * @static diff --git a/patches/fabric+4.6.0.patch b/patches/fabric+4.6.0.patch new file mode 100644 index 000000000..5418a45cf --- /dev/null +++ b/patches/fabric+4.6.0.patch @@ -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 diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 4a76d08cd..4df7e1164 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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; } } } diff --git a/stylesheets/components/ContextMenu.scss b/stylesheets/components/ContextMenu.scss new file mode 100644 index 000000000..a94bbefd7 --- /dev/null +++ b/stylesheets/components/ContextMenu.scss @@ -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; + } + } +} diff --git a/stylesheets/components/MediaEditor.scss b/stylesheets/components/MediaEditor.scss new file mode 100644 index 000000000..71510df3f --- /dev/null +++ b/stylesheets/components/MediaEditor.scss @@ -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); + } + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 148b59302..32411a387 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -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'; diff --git a/ts/components/Button.tsx b/ts/components/Button.tsx index adf77a0fc..4281964c9 100644 --- a/ts/components/Button.tsx +++ b/ts/components/Button.tsx @@ -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( icon, style, tabIndex, + theme, variant = ButtonVariant.Primary, size = variant === ButtonVariant.Details ? ButtonSize.Small @@ -120,7 +124,7 @@ export const Button = React.forwardRef( const variantClassName = VARIANT_CLASS_NAMES.get(variant); assert(variantClassName, ' ); + + if (theme) { + return
{buttonElement}
; + } + + return buttonElement; } ); diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index 5baa1679f..f7c7d62ac 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -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(); const fileInputRef = useRef(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 (
+ {attachmentToEdit && 'url' in attachmentToEdit && attachmentToEdit.url && ( + setAttachmentToEdit(undefined)} + onDone={data => { + const newAttachment = { + ...attachmentToEdit, + contentType: IMAGE_PNG, + data, + size: data.byteLength, + }; + + addAttachment(conversationId, newAttachment); + setAttachmentToEdit(undefined); + }} + installedPacks={installedPacks} + recentStickers={recentStickers} + /> + )}
+ ))} +
+ )} +
+ ); +}; diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index b6ce37cb4..f7e59875d 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -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: '', }, })} /> diff --git a/ts/components/MediaEditor.stories.tsx b/ts/components/MediaEditor.stories.tsx new file mode 100644 index 000000000..3f80759f3 --- /dev/null +++ b/ts/components/MediaEditor.stories.tsx @@ -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', () => ); + +story.add('Large', () => ( + +)); + +story.add('Smol', () => ( + +)); + +story.add('Portrait', () => ( + +)); diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx new file mode 100644 index 000000000..df285be23 --- /dev/null +++ b/ts/components/MediaEditor.tsx @@ -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; + +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(); + const [image, setImage] = useState(new Image()); + + const isRestoringImageState = useRef(false); + + const canvasId = useUniqueId(); + + const [imageState, setImageState] = useState({ + 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(': 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.Pen); + const [drawWidth, setDrawWidth] = useState(DrawWidth.Regular); + const [editMode, setEditMode] = useState(); + const [sliderValue, setSliderValue] = useState(0); + const [textStyle, setTextStyle] = useState(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 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 = ( + <> + + setTextStyle(value)} + theme={Theme.Dark} + value={textStyle} + /> + + ); + } else if (editMode === EditMode.Draw) { + tooling = ( + <> + + setDrawTool(value)} + theme={Theme.Dark} + value={drawTool} + /> + 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 = ( +
+ + +
+ ); + } + + return createPortal( +
+
+ { + if (!bounds) { + log.error('We should be measuring the bounds'); + return; + } + setContainerWidth(bounds.width); + setContainerHeight(bounds.height); + }} + > + {({ measureRef }) => ( +
+ {image && ( +
+ +
+ )} +
+ )} +
+
+
+ {tooling ? ( +
{tooling}
+ ) : ( +
+ )} +
+ +
+
+ +
+
+
, + portal + ); +}; diff --git a/ts/components/conversation/AttachmentList.stories.tsx b/ts/components/conversation/AttachmentList.stories.tsx index 30c81bd07..ef4692969 100644 --- a/ts/components/conversation/AttachmentList.stories.tsx +++ b/ts/components/conversation/AttachmentList.stories.tsx @@ -43,7 +43,7 @@ story.add('One File', () => { }), ], }); - return ; + return ; }); story.add('Multiple Visual Attachments', () => { diff --git a/ts/components/conversation/AttachmentList.tsx b/ts/components/conversation/AttachmentList.tsx index dc790615d..ac6006b97 100644 --- a/ts/components/conversation/AttachmentList.tsx +++ b/ts/components/conversation/AttachmentList.tsx @@ -17,6 +17,7 @@ import { export type Props = Readonly<{ attachments: ReadonlyArray; + 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 = ( {i18n('stagedImageAttachment', ); + + if (isImage && canEditImages) { + return ( +
+ {imgElement} +
+
+ ); + } + + return imgElement; } return ( diff --git a/ts/components/leftPane/LeftPaneSearchHelper.tsx b/ts/components/leftPane/LeftPaneSearchHelper.tsx index ee88c9cc9..affa8afd1 100644 --- a/ts/components/leftPane/LeftPaneSearchHelper.tsx +++ b/ts/components/leftPane/LeftPaneSearchHelper.tsx @@ -43,6 +43,8 @@ const searchResultKeys: Array< 'conversationResults' | 'contactResults' | 'messageResults' > = ['conversationResults', 'contactResults', 'messageResults']; +/* eslint-disable class-methods-use-this */ + export class LeftPaneSearchHelper extends LeftPaneHelper { private readonly conversationResults: MaybeLoadedSearchResultsType; diff --git a/ts/components/stickers/StickerButton.tsx b/ts/components/stickers/StickerButton.tsx index b8879cca7..d3938d28a 100644 --- a/ts/components/stickers/StickerButton.tsx +++ b/ts/components/stickers/StickerButton.tsx @@ -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; readonly installedPacks: ReadonlyArray; @@ -21,19 +25,25 @@ export type OwnProps = { readonly installedPack?: StickerPackType | null; readonly recentStickers: ReadonlyArray; 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( @@ -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 }) => ( - + + + {installedPack.title} + {' '} + installed + +
+ +
)} ) : null} {!open && showIntroduction ? ( {({ ref, style, placement, arrowProps }) => ( -
-
+ + onClick={handleClearIntroduction} + > + {i18n('stickers--StickerManager--Introduction--Image')} +
+
+ {i18n('stickers--StickerManager--Introduction--Title')} +
+
+ {i18n('stickers--StickerManager--Introduction--Body')} +
+
+
+
+
+ +
)} ) : null} @@ -294,17 +315,21 @@ export const StickerButton = React.memo( ? createPortal( {({ ref, style }) => ( - +
+ +
)}
, popperRoot diff --git a/ts/components/stickers/StickerPicker.tsx b/ts/components/stickers/StickerPicker.tsx index 9c978cf0f..f0407ff51 100644 --- a/ts/components/stickers/StickerPicker.tsx +++ b/ts/components/stickers/StickerPicker.tsx @@ -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; readonly recentStickers: ReadonlyArray; readonly showPickerHint?: boolean; @@ -230,20 +234,22 @@ export const StickerPicker = React.memo( /> ) : null}
-
onPickSticker(packId, id)} + onClick={() => onPickSticker(packId, id, url)} > (null); + + useEffect(() => { + const div = document.createElement('div'); + document.body.appendChild(div); + setRoot(div); + + return () => { + document.body.removeChild(div); + setRoot(null); + }; + }, []); + + return root; +} diff --git a/ts/hooks/useUniqueId.ts b/ts/hooks/useUniqueId.ts new file mode 100644 index 000000000..c6b099edc --- /dev/null +++ b/ts/hooks/useUniqueId.ts @@ -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(), []); +} diff --git a/ts/mediaEditor/ImageStateType.ts b/ts/mediaEditor/ImageStateType.ts new file mode 100644 index 000000000..0cf5fc2fc --- /dev/null +++ b/ts/mediaEditor/ImageStateType.ts @@ -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; +}; diff --git a/ts/mediaEditor/MediaEditorFabricCropRect.ts b/ts/mediaEditor/MediaEditorFabricCropRect.ts new file mode 100644 index 000000000..4776c8041 --- /dev/null +++ b/ts/mediaEditor/MediaEditorFabricCropRect.ts @@ -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); +} diff --git a/ts/mediaEditor/MediaEditorFabricIText.ts b/ts/mediaEditor/MediaEditorFabricIText.ts new file mode 100644 index 000000000..a9db7f286 --- /dev/null +++ b/ts/mediaEditor/MediaEditorFabricIText.ts @@ -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; diff --git a/ts/mediaEditor/MediaEditorFabricPath.ts b/ts/mediaEditor/MediaEditorFabricPath.ts new file mode 100644 index 000000000..239239a20 --- /dev/null +++ b/ts/mediaEditor/MediaEditorFabricPath.ts @@ -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, + 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; diff --git a/ts/mediaEditor/MediaEditorFabricPencilBrush.ts b/ts/mediaEditor/MediaEditorFabricPencilBrush.ts new file mode 100644 index 000000000..7caa5f7ed --- /dev/null +++ b/ts/mediaEditor/MediaEditorFabricPencilBrush.ts @@ -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 + ): 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, + }); + } +} diff --git a/ts/mediaEditor/MediaEditorFabricSticker.ts b/ts/mediaEditor/MediaEditorFabricSticker.ts new file mode 100644 index 000000000..b5b442306 --- /dev/null +++ b/ts/mediaEditor/MediaEditorFabricSticker.ts @@ -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; diff --git a/ts/mediaEditor/useFabricHistory.ts b/ts/mediaEditor/useFabricHistory.ts new file mode 100644 index 000000000..65380c6dc --- /dev/null +++ b/ts/mediaEditor/useFabricHistory.ts @@ -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(); + + // 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 . + 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; + + 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; + } +} diff --git a/ts/mediaEditor/util/color.ts b/ts/mediaEditor/util/color.ts new file mode 100644 index 000000000..87b75a2bc --- /dev/null +++ b/ts/mediaEditor/util/color.ts @@ -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})`; +} diff --git a/ts/mediaEditor/util/customFabricObjectControls.ts b/ts/mediaEditor/util/customFabricObjectControls.ts new file mode 100644 index 000000000..cdfa10e67 --- /dev/null +++ b/ts/mediaEditor/util/customFabricObjectControls.ts @@ -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 . + 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, +}; diff --git a/ts/mediaEditor/util/getTextStyleAttributes.ts b/ts/mediaEditor/util/getTextStyleAttributes.ts new file mode 100644 index 000000000..e33859b4d --- /dev/null +++ b/ts/mediaEditor/util/getTextStyleAttributes.ts @@ -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); + } +} diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index cecab51e0..b9907c320 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -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 diff --git a/ts/test-both/helpers/getStickerPacks.ts b/ts/test-both/helpers/getStickerPacks.ts new file mode 100644 index 000000000..01bfa6302 --- /dev/null +++ b/ts/test-both/helpers/getStickerPacks.ts @@ -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, + 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 = { + 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), +]; diff --git a/ts/util/canEditImages.ts b/ts/util/canEditImages.ts new file mode 100644 index 000000000..142afa047 --- /dev/null +++ b/ts/util/canEditImages.ts @@ -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()) + ); +} diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 4c53537ac..83b118417 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -162,6 +162,104 @@ "reasonCategory": "falseMatch", "updated": "2021-04-05T20:48:36.065Z" }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.es.js", + "line": "\t\t\t\t\tthis.append(headerName, value);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.es.js", + "line": "\t\t// We don't worry about converting prop to ByteString here as append()", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.es.js", + "line": "\t\t\t\t\tthis.append(pair[0], pair[1]);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.es.js", + "line": "\t\t\t\t\tthis.append(key, value);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.es.js", + "line": "\tappend(name, value) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.es.js", + "line": "\t\t\t\theaders.append('Content-Type', contentType);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.es.js", + "line": "\t\t\t\theaders.append('Content-Type', contentType);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.js", + "line": "\t\t\t\t\tthis.append(headerName, value);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.js", + "line": "\t\t// We don't worry about converting prop to ByteString here as append()", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.js", + "line": "\t\t\t\t\tthis.append(pair[0], pair[1]);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.js", + "line": "\t\t\t\t\tthis.append(key, value);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.js", + "line": "\tappend(name, value) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.js", + "line": "\t\t\t\theaders.append('Content-Type', contentType);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch/lib/index.js", + "line": "\t\t\t\theaders.append('Content-Type', contentType);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "eval", "path": "node_modules/@protobufjs/inquire/index.js", @@ -540,6 +638,13 @@ "reasonCategory": "falseMatch", "updated": "2019-07-31T00:19:18.696Z" }, + { + "rule": "jQuery-$(", + "path": "node_modules/acorn-globals/node_modules/acorn/dist/acorn.js", + "line": " // $$('#table-binary-unicode-properties > figure > table > tbody > tr > td:nth-child(1) code').map(el => el.innerText)", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "jQuery-$(", "path": "node_modules/acorn/dist/acorn.js", @@ -1449,6 +1554,12 @@ "reasonCategory": "falseMatch|", "updated": "2020-04-30T22:35:27.860Z" }, + { + "rule": "jQuery-$(", + "path": "node_modules/fabric/dist/fabric.min.js", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "jQuery-load(", "path": "node_modules/file-entry-cache/cache.js", @@ -1743,94 +1854,94 @@ "path": "node_modules/intl-tel-input/build/js/intlTelInput-jquery.js", "line": " this.selectedDialCode.innerHTML = dialCode;", "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-insertBefore(", "path": "node_modules/intl-tel-input/build/js/intlTelInput-jquery.js", "line": " this.telInput.parentNode.insertBefore(wrapper, this.telInput);", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-insertBefore(", "path": "node_modules/intl-tel-input/build/js/intlTelInput-jquery.js", "line": " wrapper.parentNode.insertBefore(this.telInput, wrapper);", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "DOM-innerHTML", "path": "node_modules/intl-tel-input/build/js/intlTelInput-jquery.min.js", "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-insertBefore(", "path": "node_modules/intl-tel-input/build/js/intlTelInput-jquery.min.js", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "DOM-innerHTML", "path": "node_modules/intl-tel-input/build/js/intlTelInput.js", "line": " this.selectedDialCode.innerHTML = dialCode;", "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-insertBefore(", "path": "node_modules/intl-tel-input/build/js/intlTelInput.js", "line": " this.telInput.parentNode.insertBefore(wrapper, this.telInput);", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-insertBefore(", "path": "node_modules/intl-tel-input/build/js/intlTelInput.js", "line": " wrapper.parentNode.insertBefore(this.telInput, wrapper);", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "DOM-innerHTML", "path": "node_modules/intl-tel-input/build/js/intlTelInput.min.js", "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-insertBefore(", "path": "node_modules/intl-tel-input/build/js/intlTelInput.min.js", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "DOM-innerHTML", "path": "node_modules/intl-tel-input/src/js/intlTelInput.js", "line": " this.selectedDialCode.innerHTML = dialCode;", "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-insertBefore(", "path": "node_modules/intl-tel-input/src/js/intlTelInput.js", "line": " this.telInput.parentNode.insertBefore(wrapper, this.telInput);", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-insertBefore(", "path": "node_modules/intl-tel-input/src/js/intlTelInput.js", "line": " wrapper.parentNode.insertBefore(this.telInput, wrapper);", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-$(", "path": "node_modules/intl-tel-input/src/spec/helpers/helpers.js", "line": " $(\"script.iti-load-utils\").remove();", - "reasonCategory": "usageTrusted", - "updated": "2021-11-24T20:55:14.943Z" + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "DOM-document.write(", @@ -1985,6 +2096,643 @@ "reasonCategory": "falseMatch", "updated": "2019-06-19T20:42:32.133Z" }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/api.js", + "line": " template.innerHTML = string;", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/browser/Window.js", + "line": " this._document.body.innerHTML = \"\";", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/browser/parser/html.js", + "line": " insertBefore(parentNode, newNode, referenceNode) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/level3/xpath.js", + "line": " if (null != ctx.outerHTML) return ctx.outerHTML;", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/fetch/Headers-impl.js", + "line": " this.append(header[0], header[1]);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/fetch/Headers-impl.js", + "line": " this.append(key, init[key]);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/fetch/Headers-impl.js", + "line": " append(name, value) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/fetch/Headers-impl.js", + "line": " this.headersList.append(name, value);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/fetch/header-list.js", + "line": " append(name, value) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-after(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/CharacterData.js", + "line": " after() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-after(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/CharacterData.js", + "line": " return this[impl].after(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-before(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/CharacterData.js", + "line": " before() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-before(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/CharacterData.js", + "line": " return this[impl].before(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Document.js", + "line": " append() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Document.js", + "line": " return this[impl].append(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-prepend(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Document.js", + "line": " prepend() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-prepend(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Document.js", + "line": " return this[impl].prepend(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/DocumentFragment.js", + "line": " append() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/DocumentFragment.js", + "line": " return this[impl].append(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-prepend(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/DocumentFragment.js", + "line": " prepend() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-prepend(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/DocumentFragment.js", + "line": " return this[impl].prepend(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-after(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/DocumentType.js", + "line": " after() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-after(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/DocumentType.js", + "line": " return this[impl].after(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-before(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/DocumentType.js", + "line": " before() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-before(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/DocumentType.js", + "line": " return this[impl].before(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " get innerHTML() {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " return this[impl][\"innerHTML\"];", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " set innerHTML(V) {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " context: \"Failed to set the 'innerHTML' property on 'Element': The provided value\",", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " this[impl][\"innerHTML\"] = V;", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " innerHTML: { enumerable: true },", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " get outerHTML() {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " return this[impl][\"outerHTML\"];", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " set outerHTML(V) {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " context: \"Failed to set the 'outerHTML' property on 'Element': The provided value\",", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " this[impl][\"outerHTML\"] = V;", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " outerHTML: { enumerable: true },", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-after(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " after() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-after(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " return this[impl].after(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " append() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " return this[impl].append(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-before(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " before() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-before(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " return this[impl].before(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-prepend(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " prepend() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-prepend(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Element.js", + "line": " return this[impl].prepend(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/FormData.js", + "line": " append(name, value) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/FormData.js", + "line": " return this[impl].append(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/HTMLMediaElement.js", + "line": " load() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/HTMLMediaElement.js", + "line": " return this[impl].load();", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/HTMLTextAreaElement.js", + "line": " get wrap() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/HTMLTextAreaElement.js", + "line": " set wrap(V) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Headers.js", + "line": " append(name, value) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Headers.js", + "line": " return this[impl].append(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Node.js", + "line": " insertBefore(node, child) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/generated/Node.js", + "line": " return utils.tryWrapperForImpl(this[impl].insertBefore(...args));", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/ShadowRoot.js", + "line": " get innerHTML() {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/ShadowRoot.js", + "line": " return this[impl][\"innerHTML\"];", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/ShadowRoot.js", + "line": " set innerHTML(V) {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/ShadowRoot.js", + "line": " context: \"Failed to set the 'innerHTML' property on 'ShadowRoot': The provided value\",", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/ShadowRoot.js", + "line": " this[impl][\"innerHTML\"] = V;", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/generated/ShadowRoot.js", + "line": " innerHTML: { enumerable: true },", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/helpers/ordered-set.js", + "line": " append(item) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/helpers/ordered-set.js", + "line": " tokens.append(token);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-prepend(", + "path": "node_modules/jsdom/lib/jsdom/living/helpers/ordered-set.js", + "line": " prepend(item) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-after(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/ChildNode-impl.js", + "line": " after(...nodes) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-before(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/ChildNode-impl.js", + "line": " before(...nodes) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/DOMTokenList-impl.js", + "line": " this._tokenSet.append(token);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/DOMTokenList-impl.js", + "line": " this._tokenSet.append(token);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Document-impl.js", + "line": " tempDiv.innerHTML = text;", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Document-impl.js", + "line": " node.innerHTML = text;", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Document-impl.js", + "line": " parent.insertBefore(node, previous.nextSibling);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Element-impl.js", + "line": " get innerHTML() {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Element-impl.js", + "line": " set innerHTML(markup) {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Element-impl.js", + "line": " get outerHTML() {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-outerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Element-impl.js", + "line": " set outerHTML(markup) {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/HTMLMediaElement-impl.js", + "line": " load() {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-document.write(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/HTMLScriptElement-impl.js", + "line": " // In our current terribly-hacky document.write() implementation, we parse in a div them move elements into the main", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/HTMLTableElement-impl.js", + "line": " this.insertBefore(value, insertionPoint);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/HTMLTableElement-impl.js", + "line": " this.insertBefore(value, insertionPoint);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/HTMLTableElement-impl.js", + "line": " this.insertBefore(el, insertionPoint.nextSibling);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/HTMLTableElement-impl.js", + "line": " tSection.insertBefore(tr, beforeTR);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Node-impl.js", + "line": " insertBefore(nodeImpl, childImpl) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/Node-impl.js", + "line": " domSymbolTree.insertBefore(childImpl, node);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/ParentNode-impl.js", + "line": " append(...nodes) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-prepend(", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/ParentNode-impl.js", + "line": " prepend(...nodes) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/ShadowRoot-impl.js", + "line": " get innerHTML() {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/jsdom/lib/jsdom/living/nodes/ShadowRoot-impl.js", + "line": " set innerHTML(markup) {", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "eval", + "path": "node_modules/jsdom/lib/jsdom/living/window/navigation.js", + "line": " return window.eval(scriptSource);", + "reasonCategory": "notExercisedByOurApp", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/xhr-utils.js", + "line": " form.append(entry.name, entry.value, entry.options);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/jsdom/lib/jsdom/living/xhr/FormData-impl.js", + "line": " append(name, value, filename) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/jsdom/node_modules/acorn/dist/acorn.js", + "line": " // $$('#table-binary-unicode-properties > figure > table > tbody > tr > td:nth-child(1) code').map(el => el.innerText)", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "jQuery-$(", "path": "node_modules/lazy-universal-dotenv/node_modules/core-js/internals/collection.js", @@ -4739,6 +5487,13 @@ "reasonCategory": "falseMatch", "updated": "2019-07-19T17:16:02.404Z" }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/nwsapi/src/nwsapi.js", + "line": " r = d.documentElement; r.removeChild(r.insertBefore(s, r.firstChild));", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "DOM-innerHTML", "path": "node_modules/package-json/node_modules/@sindresorhus/is/dist/index.js", @@ -5089,6 +5844,34 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, + { + "rule": "jQuery-insertAfter(", + "path": "node_modules/parse5/lib/parser/index.js", + "line": " p.openElements.insertAfter(furthestBlock, newElement);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/parse5/lib/parser/index.js", + "line": " this.treeAdapter.insertBefore(location.parent, element, location.beforeElement);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertAfter(", + "path": "node_modules/parse5/lib/parser/open-element-stack.js", + "line": " insertAfter(referenceElement, newElement) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/parse5/lib/tree-adapters/default.js", + "line": " insertBefore(parentNode, createTextNode(text), referenceNode);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "jQuery-append(", "path": "node_modules/picomatch/lib/parse.js", @@ -5101,21 +5884,21 @@ "path": "node_modules/picomatch/lib/parse.js", "line": " append({ value });", "reasonCategory": "falseMatch", - "updated": "2020-02-21T14:09:28.005Z" + "updated": "2021-04-06T04:01:59.934Z" }, { "rule": "jQuery-append(", "path": "node_modules/picomatch/lib/parse.js", "line": " append({ value });", "reasonCategory": "falseMatch", - "updated": "2020-02-21T14:09:28.005Z" + "updated": "2021-12-01T01:35:52.592Z" }, { "rule": "jQuery-append(", "path": "node_modules/picomatch/lib/parse.js", "line": " append({ value });", "reasonCategory": "falseMatch", - "updated": "2020-02-21T14:09:28.005Z" + "updated": "2021-12-01T01:35:52.592Z" }, { "rule": "thenify-multiArgs", @@ -5552,13 +6335,6 @@ "reasonCategory": "falseMatch", "updated": "2021-04-06T04:01:59.934Z" }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/playwright/lib/third_party/highlightjs/highlightjs/core.js", - "line": " domProps: { innerHTML: this.highlighted }", - "reasonCategory": "falseMatch", - "updated": "2021-04-06T04:01:59.934Z" - }, { "rule": "DOM-innerHTML", "path": "node_modules/playwright/lib/third_party/highlightjs/highlightjs/core.js", @@ -5580,6 +6356,13 @@ "reasonCategory": "falseMatch", "updated": "2021-04-06T04:01:59.934Z" }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/playwright/lib/third_party/highlightjs/highlightjs/core.js", + "line": " domProps: { innerHTML: this.highlighted }", + "reasonCategory": "usageTrusted", + "updated": "2021-12-01T01:31:12.757Z" + }, { "rule": "jQuery-$(", "path": "node_modules/playwright/lib/third_party/highlightjs/highlightjs/languages/javascript.js", @@ -7570,7 +8353,7 @@ { "rule": "DOM-innerHTML", "path": "node_modules/quill/modules/clipboard.js", - "line": " debug.log('convert', this.container.innerHTML, delta);", + "line": " this.container.innerHTML = '';", "reasonCategory": "usageTrusted", "updated": "2020-10-13T18:36:57.012Z", "reasonDetail": "necessary for quill" @@ -7578,10 +8361,9 @@ { "rule": "DOM-innerHTML", "path": "node_modules/quill/modules/clipboard.js", - "line": " this.container.innerHTML = '';", + "line": " debug.log('convert', this.container.innerHTML, delta);", "reasonCategory": "usageTrusted", - "updated": "2020-10-13T18:36:57.012Z", - "reasonDetail": "necessary for quill" + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "DOM-innerHTML", @@ -7941,7 +8723,7 @@ "rule": "jQuery-prepend(", "path": "node_modules/source-map/dist/source-map.min.js", "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "jQuery-prepend(", @@ -7978,6 +8760,34 @@ "reasonCategory": "falseMatch", "updated": "2020-04-25T01:47:02.583Z" }, + { + "rule": "jQuery-insertAfter(", + "path": "node_modules/symbol-tree/lib/SymbolTree.js", + "line": " insertAfter(referenceObject, newObject) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertAfter(", + "path": "node_modules/symbol-tree/lib/SymbolTree.js", + "line": " this.insertAfter(referenceNode.lastChild, newObject);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/symbol-tree/lib/SymbolTree.js", + "line": " insertBefore(referenceObject, newObject) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/symbol-tree/lib/SymbolTree.js", + "line": " this.insertBefore(referenceNode.firstChild, newObject);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "jQuery-append(", "path": "node_modules/table/dist/createStream.js", @@ -8319,6 +9129,27 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, + { + "rule": "jQuery-append(", + "path": "node_modules/whatwg-url/lib/URLSearchParams-impl.js", + "line": " append(name, value) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/whatwg-url/lib/URLSearchParams.js", + "line": " append(name, value) {", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, + { + "rule": "jQuery-append(", + "path": "node_modules/whatwg-url/lib/URLSearchParams.js", + "line": " return this[impl].append(...args);", + "reasonCategory": "falseMatch", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "eval", "path": "node_modules/workerpool/dist/worker.js", @@ -8560,7 +9391,7 @@ "path": "ts/components/CallScreen.tsx", "line": " const localVideoRef = useRef(null);", "reasonCategory": "usageTrusted", - "updated": "2021-07-30T16:57:33.618Z" + "updated": "2021-12-01T01:31:12.757Z" }, { "rule": "React-useRef", @@ -8941,6 +9772,13 @@ "updated": "2020-02-14T20:02:37.507Z", "reasonDetail": "Used only to set focus" }, + { + "rule": "React-useRef", + "path": "ts/components/MediaEditor.tsx", + "line": " const isRestoringImageState = useRef(false);", + "reasonCategory": "usageTrusted", + "updated": "2021-12-01T01:13:59.892Z" + }, { "rule": "React-useRef", "path": "ts/components/Modal.tsx", diff --git a/yarn.lock b/yarn.lock index 5b7a2d3a9..059b76a30 100644 --- a/yarn.lock +++ b/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"