From 11d47a8eb9f91eb85bd76a97282925a657017529 Mon Sep 17 00:00:00 2001 From: Ken Powers Date: Tue, 17 Dec 2019 15:25:57 -0500 Subject: [PATCH] Sticker Creator --- .babelrc.js | 8 + .eslintignore | 1 + .gitignore | 6 + .prettierignore | 1 + .storybook/addons.js | 2 + .storybook/config.js | 37 + .storybook/preview-head.html | 2 + .storybook/styles.scss | 21 + .storybook/webpack.config.js | 25 + CONTRIBUTING.md | 21 +- Gruntfile.js | 1 + _locales/en/messages.json | 221 + app/config.js | 8 +- app/menu.js | 58 +- app/protocol_filter.js | 13 +- js/background.js | 6 + js/modules/web_api.js | 124 +- main.js | 100 +- package.json | 73 +- permissions_popup_preload.js | 29 +- preload.js | 52 +- preload_utils.js | 74 + sticker-creator/_mixins.scss | 9 + sticker-creator/app/index.scss | 16 + sticker-creator/app/index.tsx | 38 + sticker-creator/app/stages/AppStage.scss | 41 + sticker-creator/app/stages/AppStage.tsx | 81 + sticker-creator/app/stages/DropStage.scss | 24 + sticker-creator/app/stages/DropStage.tsx | 79 + sticker-creator/app/stages/EmojiStage.tsx | 26 + sticker-creator/app/stages/MetaStage.scss | 72 + sticker-creator/app/stages/MetaStage.tsx | 112 + sticker-creator/app/stages/ShareStage.scss | 24 + sticker-creator/app/stages/ShareStage.tsx | 92 + sticker-creator/app/stages/UploadStage.scss | 10 + sticker-creator/app/stages/UploadStage.tsx | 64 + sticker-creator/components/ConfirmModal.scss | 11 + sticker-creator/components/ConfirmModal.tsx | 44 + sticker-creator/components/ShareButtons.scss | 27 + .../components/ShareButtons.stories.tsx | 16 + sticker-creator/components/ShareButtons.tsx | 70 + sticker-creator/components/StickerFrame.scss | 140 + .../components/StickerFrame.stories.tsx | 34 + sticker-creator/components/StickerFrame.tsx | 271 + sticker-creator/components/StickerGrid.scss | 13 + sticker-creator/components/StickerGrid.tsx | 110 + .../components/StickerPackPreview.scss | 126 + .../components/StickerPackPreview.stories.tsx | 22 + .../components/StickerPackPreview.tsx | 34 + sticker-creator/elements/Button.scss | 80 + sticker-creator/elements/Button.stories.tsx | 55 + sticker-creator/elements/Button.tsx | 39 + sticker-creator/elements/ConfirmDialog.scss | 98 + .../elements/ConfirmDialog.stories.tsx | 29 + sticker-creator/elements/ConfirmDialog.tsx | 39 + sticker-creator/elements/CopyText.scss | 36 + sticker-creator/elements/CopyText.stories.tsx | 17 + sticker-creator/elements/CopyText.tsx | 39 + sticker-creator/elements/DropZone.scss | 61 + sticker-creator/elements/DropZone.stories.tsx | 9 + sticker-creator/elements/DropZone.tsx | 65 + sticker-creator/elements/LabeledCheckbox.scss | 50 + .../elements/LabeledCheckbox.stories.tsx | 19 + sticker-creator/elements/LabeledCheckbox.tsx | 43 + sticker-creator/elements/LabeledInput.scss | 55 + .../elements/LabeledInput.stories.tsx | 20 + sticker-creator/elements/LabeledInput.tsx | 34 + sticker-creator/elements/MessageBubble.scss | 13 + .../elements/MessageBubble.stories.tsx | 17 + sticker-creator/elements/MessageBubble.tsx | 16 + sticker-creator/elements/MessageMeta.scss | 33 + sticker-creator/elements/MessageMeta.tsx | 54 + sticker-creator/elements/MessageSticker.scss | 10 + .../elements/MessageSticker.stories.tsx | 17 + sticker-creator/elements/MessageSticker.tsx | 16 + sticker-creator/elements/PageHeader.scss | 22 + .../elements/PageHeader.stories.tsx | 16 + sticker-creator/elements/PageHeader.tsx | 11 + sticker-creator/elements/ProgressBar.scss | 24 + .../elements/ProgressBar.stories.tsx | 17 + sticker-creator/elements/ProgressBar.tsx | 17 + sticker-creator/elements/StickerPreview.scss | 120 + .../elements/StickerPreview.stories.tsx | 16 + sticker-creator/elements/StickerPreview.tsx | 90 + sticker-creator/elements/StoryRow.scss | 28 + sticker-creator/elements/StoryRow.tsx | 34 + sticker-creator/elements/Toast.scss | 14 + sticker-creator/elements/Toast.stories.tsx | 16 + sticker-creator/elements/Toast.tsx | 12 + sticker-creator/elements/Typography.scss | 65 + .../elements/Typography.stories.tsx | 34 + sticker-creator/elements/Typography.tsx | 52 + sticker-creator/elements/icons/AddEmoji.tsx | 7 + sticker-creator/elements/icons/index.tsx | 1 + sticker-creator/index.html | 13 + sticker-creator/index.tsx | 10 + sticker-creator/preload.js | 159 + sticker-creator/root.tsx | 24 + sticker-creator/store/ducks/stickers.ts | 250 + sticker-creator/store/index.ts | 8 + sticker-creator/store/reducer.ts | 8 + sticker-creator/util/history.ts | 3 + sticker-creator/util/i18n.tsx | 30 + sticker-creator/util/preload.ts | 27 + stylesheets/_fontfaces.scss | 33 + stylesheets/_modules.scss | 2 + stylesheets/_options.scss | 1 + stylesheets/_variables.scss | 34 - stylesheets/manifest.scss | 1 + stylesheets/manifest_bridge.scss | 5 + test/app/fixtures/menu-mac-os-setup.json | 7 + test/app/fixtures/menu-mac-os.json | 9 + .../fixtures/menu-windows-linux-setup.json | 4 + test/app/fixtures/menu-windows-linux.json | 4 + test/app/menu_test.js | 1 + ts/components/emoji/Emoji.tsx | 2 +- ts/components/emoji/EmojiPicker.tsx | 10 +- ts/components/emoji/lib.ts | 11 +- ts/util/lint/exceptions.json | 1760 ++--- ts/util/lint/linter.ts | 108 +- tsconfig.json | 3 +- webpack.config.ts | 90 + yarn.lock | 6335 ++++++++++++++++- 123 files changed, 11287 insertions(+), 1714 deletions(-) create mode 100644 .babelrc.js create mode 100644 .storybook/addons.js create mode 100644 .storybook/config.js create mode 100644 .storybook/preview-head.html create mode 100644 .storybook/styles.scss create mode 100644 .storybook/webpack.config.js create mode 100644 preload_utils.js create mode 100644 sticker-creator/_mixins.scss create mode 100644 sticker-creator/app/index.scss create mode 100644 sticker-creator/app/index.tsx create mode 100644 sticker-creator/app/stages/AppStage.scss create mode 100644 sticker-creator/app/stages/AppStage.tsx create mode 100644 sticker-creator/app/stages/DropStage.scss create mode 100644 sticker-creator/app/stages/DropStage.tsx create mode 100644 sticker-creator/app/stages/EmojiStage.tsx create mode 100644 sticker-creator/app/stages/MetaStage.scss create mode 100644 sticker-creator/app/stages/MetaStage.tsx create mode 100644 sticker-creator/app/stages/ShareStage.scss create mode 100644 sticker-creator/app/stages/ShareStage.tsx create mode 100644 sticker-creator/app/stages/UploadStage.scss create mode 100644 sticker-creator/app/stages/UploadStage.tsx create mode 100644 sticker-creator/components/ConfirmModal.scss create mode 100644 sticker-creator/components/ConfirmModal.tsx create mode 100644 sticker-creator/components/ShareButtons.scss create mode 100644 sticker-creator/components/ShareButtons.stories.tsx create mode 100644 sticker-creator/components/ShareButtons.tsx create mode 100644 sticker-creator/components/StickerFrame.scss create mode 100644 sticker-creator/components/StickerFrame.stories.tsx create mode 100644 sticker-creator/components/StickerFrame.tsx create mode 100644 sticker-creator/components/StickerGrid.scss create mode 100644 sticker-creator/components/StickerGrid.tsx create mode 100644 sticker-creator/components/StickerPackPreview.scss create mode 100644 sticker-creator/components/StickerPackPreview.stories.tsx create mode 100644 sticker-creator/components/StickerPackPreview.tsx create mode 100644 sticker-creator/elements/Button.scss create mode 100644 sticker-creator/elements/Button.stories.tsx create mode 100644 sticker-creator/elements/Button.tsx create mode 100644 sticker-creator/elements/ConfirmDialog.scss create mode 100644 sticker-creator/elements/ConfirmDialog.stories.tsx create mode 100644 sticker-creator/elements/ConfirmDialog.tsx create mode 100644 sticker-creator/elements/CopyText.scss create mode 100644 sticker-creator/elements/CopyText.stories.tsx create mode 100644 sticker-creator/elements/CopyText.tsx create mode 100644 sticker-creator/elements/DropZone.scss create mode 100644 sticker-creator/elements/DropZone.stories.tsx create mode 100644 sticker-creator/elements/DropZone.tsx create mode 100644 sticker-creator/elements/LabeledCheckbox.scss create mode 100644 sticker-creator/elements/LabeledCheckbox.stories.tsx create mode 100644 sticker-creator/elements/LabeledCheckbox.tsx create mode 100644 sticker-creator/elements/LabeledInput.scss create mode 100644 sticker-creator/elements/LabeledInput.stories.tsx create mode 100644 sticker-creator/elements/LabeledInput.tsx create mode 100644 sticker-creator/elements/MessageBubble.scss create mode 100644 sticker-creator/elements/MessageBubble.stories.tsx create mode 100644 sticker-creator/elements/MessageBubble.tsx create mode 100644 sticker-creator/elements/MessageMeta.scss create mode 100644 sticker-creator/elements/MessageMeta.tsx create mode 100644 sticker-creator/elements/MessageSticker.scss create mode 100644 sticker-creator/elements/MessageSticker.stories.tsx create mode 100644 sticker-creator/elements/MessageSticker.tsx create mode 100644 sticker-creator/elements/PageHeader.scss create mode 100644 sticker-creator/elements/PageHeader.stories.tsx create mode 100644 sticker-creator/elements/PageHeader.tsx create mode 100644 sticker-creator/elements/ProgressBar.scss create mode 100644 sticker-creator/elements/ProgressBar.stories.tsx create mode 100644 sticker-creator/elements/ProgressBar.tsx create mode 100644 sticker-creator/elements/StickerPreview.scss create mode 100644 sticker-creator/elements/StickerPreview.stories.tsx create mode 100644 sticker-creator/elements/StickerPreview.tsx create mode 100644 sticker-creator/elements/StoryRow.scss create mode 100644 sticker-creator/elements/StoryRow.tsx create mode 100644 sticker-creator/elements/Toast.scss create mode 100644 sticker-creator/elements/Toast.stories.tsx create mode 100644 sticker-creator/elements/Toast.tsx create mode 100644 sticker-creator/elements/Typography.scss create mode 100644 sticker-creator/elements/Typography.stories.tsx create mode 100644 sticker-creator/elements/Typography.tsx create mode 100644 sticker-creator/elements/icons/AddEmoji.tsx create mode 100644 sticker-creator/elements/icons/index.tsx create mode 100644 sticker-creator/index.html create mode 100644 sticker-creator/index.tsx create mode 100644 sticker-creator/preload.js create mode 100644 sticker-creator/root.tsx create mode 100644 sticker-creator/store/ducks/stickers.ts create mode 100644 sticker-creator/store/index.ts create mode 100644 sticker-creator/store/reducer.ts create mode 100644 sticker-creator/util/history.ts create mode 100644 sticker-creator/util/i18n.tsx create mode 100644 sticker-creator/util/preload.ts create mode 100644 stylesheets/_fontfaces.scss create mode 100644 stylesheets/manifest_bridge.scss create mode 100644 webpack.config.ts diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 000000000..0bf885695 --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,8 @@ +module.exports = { + presets: ['@babel/preset-react', '@babel/preset-typescript'], + plugins: [ + 'react-hot-loader/babel', + 'lodash', + '@babel/plugin-proposal-class-properties', + ], +}; diff --git a/.eslintignore b/.eslintignore index 478d2878c..65400c161 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,6 +12,7 @@ js/libsignal-protocol-worker.js libtextsecure/components.js libtextsecure/test/test.js test/test.js +sticker-creator/dist/** # Third-party files js/Mp3LameEncoder.min.js diff --git a/.gitignore b/.gitignore index b1b7020d1..55f7c5e48 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,9 @@ test/test.js # React / TypeScript ts/**/*.js ts/protobuf/*.d.ts + +# CSS Modules +**/*.scss.d.ts + +# Sticker Creator +sticker-creator/dist/* diff --git a/.prettierignore b/.prettierignore index 9935ce01c..c9bf7cdb6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,6 +17,7 @@ ts/protobuf/*.d.ts ts/protobuf/*.js stylesheets/manifest.css ts/util/lint/exceptions.json +sticker-creator/dist/** # Third-party files node_modules/** diff --git a/.storybook/addons.js b/.storybook/addons.js new file mode 100644 index 000000000..2efe80ebf --- /dev/null +++ b/.storybook/addons.js @@ -0,0 +1,2 @@ +import '@storybook/addon-knobs/register'; +import '@storybook/addon-actions/register'; diff --git a/.storybook/config.js b/.storybook/config.js new file mode 100644 index 000000000..23584c9eb --- /dev/null +++ b/.storybook/config.js @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { addDecorator, configure } from '@storybook/react'; +import { withKnobs } from '@storybook/addon-knobs'; +import classnames from 'classnames'; +import * as styles from './styles.scss'; +import messages from '../_locales/en/messages.json'; +import { I18n } from '../sticker-creator/util/i18n'; + +addDecorator(withKnobs); + +addDecorator((storyFn /* , context */) => { + const contents = storyFn(); + + return ( +
+
{contents}
+
+ {contents} +
+
+ ); +}); + +// Hack to enable hooks in stories: https://github.com/storybookjs/storybook/issues/5721#issuecomment-473869398 +addDecorator(Story => ); + +addDecorator(story => {story()}); + +configure(() => { + // Load sticker creator stories + const stickerCreatorContext = require.context( + '../sticker-creator', + true, + /\.stories\.tsx?$/ + ); + stickerCreatorContext.keys().forEach(f => stickerCreatorContext(f)); +}, module); diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 000000000..76e0bd779 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,2 @@ + + diff --git a/.storybook/styles.scss b/.storybook/styles.scss new file mode 100644 index 000000000..3fc8c681e --- /dev/null +++ b/.storybook/styles.scss @@ -0,0 +1,21 @@ +.container { + display: flex; + flex-direction: row; + align-items: stretch; + align-content: stretch; + width: 100vw; + height: 100vh; +} + +.panel { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + padding: 16px; +} + +.dark-theme { + background-color: #17191d; +} diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js new file mode 100644 index 000000000..e4465f9da --- /dev/null +++ b/.storybook/webpack.config.js @@ -0,0 +1,25 @@ +module.exports = ({ config }) => { + config.entry.unshift( + '!!style-loader!css-loader!sanitize.css', + '!!style-loader!css-loader!typeface-inter' + ); + + config.module.rules.unshift( + { + test: /\.tsx?$/, + loader: 'babel-loader', + }, + { + test: /\.scss$/, + loaders: [ + 'style-loader', + 'css-loader?modules=true&localsConvention=camelCaseOnly', + 'sass-loader', + ], + } + ); + + config.resolve.extensions = ['.tsx', '.ts', '.jsx', '.js']; + + return config; +}; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73503d1af..dfde8ec1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,7 @@ npm install --global yarn # (only if you don’t already have `yarn`) yarn install --frozen-lockfile # Install and build dependencies (this will take a while) yarn grunt # Generate final JS and CSS assets yarn icon-gen # Generate full set of icons for Electron +yarn build:webpack # Build parts of the app that use webpack (Sticker Creator) yarn test # A good idea to make sure tests run first yarn start # Start Signal! ``` @@ -76,6 +77,24 @@ while you make changes: yarn grunt dev # runs until you stop it, re-generating built assets on file changes ``` +### webpack + +Some parts of the app (such as the Sticker Creator) have moved to webpack. +You can run a development server for these parts of the app with the +following command: + +``` +yarn dev +``` + +In order for the app to make requests to the development server you must set +the `SIGNAL_ENABLE_HTTP` environment variable to a truthy value. On Linux and +macOS, that simply looks like this: + +``` +SIGNAL_ENABLE_HTTP=1 yarn start +``` + ## Setting up standalone By default the application will connect to the **staging** servers, which means that you @@ -261,7 +280,7 @@ To test changes to the build system, build a release using ``` yarn generate -yarn build-release +yarn build ``` Then, run the tests using `grunt test-release:osx --dir=release`, replacing `osx` with `linux` or `win` depending on your platform. diff --git a/Gruntfile.js b/Gruntfile.js index 6e01da3a0..a898a7362 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -98,6 +98,7 @@ module.exports = grunt => { dev: { files: { 'stylesheets/manifest.css': 'stylesheets/manifest.scss', + 'stylesheets/manifest_bridge.css': 'stylesheets/manifest_bridge.scss', }, }, }, diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 73f4f54c1..e520ce83a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -28,6 +28,11 @@ "description": "The label that is used for the File menu in the program main menu. The '&' indicates that the following letter will be used as the keyboard 'shortcut letter' for accessing the menu with the Alt- combination." }, + "mainMenuCreateStickers": { + "message": "Create/upload sticker pack", + "description": + "The label that is used for the Create/upload sticker pack option in the File menu in the program main menu. The '&' indicates that the following letter will be used as the keyboard 'shortcut letter' for accessing the menu with the Alt- combination." + }, "mainMenuEdit": { "message": "&Edit", "description": @@ -716,6 +721,11 @@ "description": "Title of the window that pops up with Signal Desktop preferences in it" }, + "signalDesktopStickerCreator": { + "message": "Sticker pack creator", + "description": + "Title of the window that pops up with Signal Desktop preferences in it" + }, "aboutSignalDesktop": { "message": "About Signal Desktop", "description": "Item under the Help menu, which opens a small about window" @@ -2142,5 +2152,216 @@ "message": "Conversation returned to inbox", "description": "A toast that shows up when the user unarchives a conversation" + }, + "StickerCreator--title": { + "message": "Sticker pack creator", + "description": "The title of the Sticker Pack Creator window" + }, + "StickerCreator--DropZone--staticText": { + "message": "Click to add or drop images here", + "description": + "Text which appears on the Sticker Creator drop zone when there is no active drag" + }, + "StickerCreator--DropZone--activeText": { + "message": "Drop images here", + "description": + "Text which appears on the Sticker Creator drop zone when there is an active drag" + }, + "StickerCreator--Preview--title": { + "message": "Sticker pack", + "description": "The 'title' of the sticker pack preview 'modal'" + }, + "StickerCreator--ConfirmDialog--cancel": { + "message": "Cancel", + "description": "The default text for the confirm dialog cancel button" + }, + "StickerCreator--CopyText--button": { + "message": "Copy", + "description": + "The text which appears on the copy button for the sticker creator share screen" + }, + "StickerCreator--ShareButtons--facebook": { + "message": "Facebook", + "description": "Title for Facebook button" + }, + "StickerCreator--ShareButtons--twitter": { + "message": "Twitter", + "description": "Title for Twitter button" + }, + "StickerCreator--ShareButtons--pinterest": { + "message": "Pinterest", + "description": "Title for Pinterest button" + }, + "StickerCreator--ShareButtons--whatsapp": { + "message": "WhatsApp", + "description": "Title for WhatsApp button" + }, + "StickerCreator--AppStage--next": { + "message": "Next", + "description": + "Default text for the next button on all stages of the sticker creator" + }, + "StickerCreator--AppStage--prev": { + "message": "Back", + "description": + "Default text for the previous button on all stages of the sticker creator" + }, + "StickerCreator--DropStage--title": { + "message": "Add your stickers", + "description": "Title for the drop stage of the sticker creator" + }, + "StickerCreator--DropStage--help": { + "message": + "Stickers must be in PNG format with a transparent background and 512x512 pixels. Recommended margin is 16px.", + "description": "Help text for the drop stage of the sticker creator" + }, + "StickerCreator--DropStage--showMargins": { + "message": "Show margins", + "description": + "Text for the show margins toggle on the drop stage of the sticker creator" + }, + "StickerCreator--EmojiStage--title": { + "message": "Add an emoji to each sticker", + "description": "Title for the drop stage of the sticker creator" + }, + "StickerCreator--EmojiStage--help": { + "message": "This allows us to suggest stickers to you as you're messaging.", + "description": "Help text for the drop stage of the sticker creator" + }, + "StickerCreator--MetaStage--title": { + "message": "Just a few more details...", + "description": "Title for the meta stage of the sticker creator" + }, + "StickerCreator--MetaStage--Field--title": { + "message": "Title", + "description": + "Label for the title input of the meta stage of the sticker creator" + }, + "StickerCreator--MetaStage--Field--author": { + "message": "Author", + "description": + "Label for the author input of the meta stage of the sticker creator" + }, + "StickerCreator--MetaStage--Field--cover": { + "message": "Cover image", + "description": + "Label for the cover image picker of the meta stage of the sticker creator" + }, + "StickerCreator--MetaStage--Field--cover--help": { + "message": + "This is the image that will show up when you share your sticker pack", + "description": + "Help text for the cover image picker of the meta stage of the sticker creator" + }, + "StickerCreator--MetaStage--ConfirmDialog--title": { + "message": "Are you sure you want to upload your sticker pack?", + "description": + "Title for the confirm dialog on the meta stage of the sticker creator" + }, + "StickerCreator--MetaStage--ConfirmDialog--confirm": { + "message": "Upload", + "description": + "Text for the upload button in the confirmation dialog on the meta stage of the sticker creator" + }, + "StickerCreator--MetaStage--ConfirmDialog--text": { + "message": + "You will no longer be able to make edits or delete after creating a sticker pack.", + "description": + "The text inside the confirmation dialog on the meta stage of the sticker creator" + }, + "StickerCreator--UploadStage--title": { + "message": "Creating your sticker pack", + "description": "Title for the upload stage of the sticker creator" + }, + "StickerCreator--UploadStage-uploaded": { + "message": "$count$ of $total$ uploaded", + "description": "Title for the upload stage of the sticker creator", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + }, + "total": { + "content": "$2", + "example": "20" + } + } + }, + "StickerCreator--ShareStage--title": { + "message": "Congratulations! You created a sticker pack.", + "description": "Title for the share stage of the sticker creator" + }, + "StickerCreator--ShareStage--help": { + "message": + "Access your new stickers through the sticker icon, or share with your friends using the link below.", + "description": "Help text for the share stage of the sticker creator" + }, + "StickerCreator--ShareStage--callToAction": { + "message": + "Use the hashtag $hashtag$ to help other people find the URLs for any custom sticker packs that you would like to make publicly accessible.", + "description": + "Call to action text for the share stage of the sticker creator", + "placeholders": { + "hashtag": { + "content": "$1", + "example": "#makeprivacystick" + } + } + }, + "StickerCreator--ShareStage--copyTitle": { + "message": "Sticker Pack URL", + "description": + "Title for the copy button on the share stage of the sticker creator" + }, + "StickerCreator--ShareStage--close": { + "message": "Close", + "description": + "Text for the close button on the share stage of the sticker creator" + }, + "StickerCreator--ShareStage--createAnother": { + "message": "Create another sticker pack", + "description": + "Text for the create another sticker pack button on the share stage of the sticker creator" + }, + "StickerCreator--ShareStage--socialMessage": { + "message": + "Check out this new sticker pack I created for Signal. #makeprivacystick", + "description": + "Text which is shared to social media platforms for sticker packs" + }, + "StickerCreator--Toasts--imagesAdded": { + "message": "$count$ image(s) added", + "description": + "Text for the toast when images are added to the sticker creator", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "StickerCreator--Toasts--tooLarge": { + "message": "Dropped image is too large", + "description": + "Text for the toast when an image that is too large was dropped" + }, + "StickerCreator--Toasts--linkedCopied": { + "message": "Link copied", + "description": + "Text for the toast when a link for sharing is copied from the Sticker Creator" + }, + "StickerCreator--StickerPreview--light": { + "message": "My sticker in light theme", + "description": "Text for the sticker preview for the light theme" + }, + "StickerCreator--StickerPreview--dark": { + "message": "My sticker in dark theme", + "description": "Text for the sticker preview for the dark theme" + }, + "StickerCreator--Authentication--error": { + "message": + "Please set up Signal on your phone and desktop to use the Sticker Pack Creator", + "description": + "The error message which appears when the user has not linked their account and attempts to use the Sticker Creator" } } diff --git a/app/config.js b/app/config.js index 49423916e..242078abf 100644 --- a/app/config.js +++ b/app/config.js @@ -1,11 +1,10 @@ const path = require('path'); - -const electronIsDev = require('electron-is-dev'); +const { app } = require('electron'); let environment; // In production mode, NODE_ENV cannot be customized by the user -if (electronIsDev) { +if (!app.isPackaged) { environment = process.env.NODE_ENV || 'development'; } else { environment = 'production'; @@ -24,12 +23,14 @@ if (environment === 'production') { process.env.ALLOW_CONFIG_MUTATIONS = ''; process.env.SUPPRESS_NO_CONFIG_WARNING = ''; process.env.NODE_TLS_REJECT_UNAUTHORIZED = ''; + process.env.SIGNAL_ENABLE_HTTP = ''; } // We load config after we've made our modifications to NODE_ENV const config = require('config'); config.environment = environment; +config.enableHttp = process.env.SIGNAL_ENABLE_HTTP; // Log resulting env vars in use by config [ @@ -40,6 +41,7 @@ config.environment = environment; 'HOSTNAME', 'NODE_APP_INSTANCE', 'SUPPRESS_NO_CONFIG_WARNING', + 'SIGNAL_ENABLE_HTTP', ].forEach(s => { console.log(`${s} ${config.util.getEnv(s)}`); }); diff --git a/app/menu.js b/app/menu.js index f2f470f4a..74bc4ed1b 100644 --- a/app/menu.js +++ b/app/menu.js @@ -19,12 +19,17 @@ exports.createTemplate = (options, messages) => { showDebugLog, showKeyboardShortcuts, showSettings, + showStickerCreator, } = options; const template = [ { label: messages.mainMenuFile.message, submenu: [ + { + label: messages.mainMenuCreateStickers.message, + click: showStickerCreator, + }, { label: messages.mainMenuSettings.message, accelerator: 'CommandOrControl+,', @@ -199,49 +204,18 @@ exports.createTemplate = (options, messages) => { }; function updateForMac(template, messages, options) { - const { - includeSetup, - setupAsNewDevice, - setupAsStandalone, - setupWithImport, - showAbout, - showSettings, - showWindow, - } = options; + const { showAbout, showSettings, showWindow } = options; - // Remove About item and separator from Help menu, since it's on the first menu + // Remove About item and separator from Help menu, since they're in the app menu template[4].submenu.pop(); template[4].submenu.pop(); - // Remove File menu - template.shift(); - - if (includeSetup) { - // Add a File menu just for these setup options. Because we're using unshift(), we add - // the file menu first, though it ends up to the right of the Signal Desktop menu. - const fileMenu = { - label: messages.mainMenuFile.message, - submenu: [ - { - label: messages.menuSetupWithImport.message, - click: setupWithImport, - }, - { - label: messages.menuSetupAsNewDevice.message, - click: setupAsNewDevice, - }, - ], - }; - - if (options.development) { - fileMenu.submenu.push({ - label: messages.menuSetupAsStandalone.message, - click: setupAsStandalone, - }); - } - - template.unshift(fileMenu); - } + // Remove preferences, separator, and quit from the File menu, since they're + // in the app menu + const fileMenu = template[0]; + fileMenu.submenu.pop(); + fileMenu.submenu.pop(); + fileMenu.submenu.pop(); // Add the OSX-specific Signal Desktop menu at the far left template.unshift({ @@ -285,8 +259,7 @@ function updateForMac(template, messages, options) { }); // Add to Edit menu - const editIndex = includeSetup ? 2 : 1; - template[editIndex].submenu.push( + template[2].submenu.push( { type: 'separator', }, @@ -306,9 +279,8 @@ function updateForMac(template, messages, options) { ); // Replace Window menu - const windowMenuTemplateIndex = includeSetup ? 4 : 3; // eslint-disable-next-line no-param-reassign - template[windowMenuTemplateIndex].submenu = [ + template[4].submenu = [ { label: messages.windowMenuClose.message, accelerator: 'CmdOrCtrl+W', diff --git a/app/protocol_filter.js b/app/protocol_filter.js index a065e61c8..b32b73b2d 100644 --- a/app/protocol_filter.js +++ b/app/protocol_filter.js @@ -75,7 +75,7 @@ function _disabledHandler(request, callback) { return callback(); } -function installWebHandler({ protocol }) { +function installWebHandler({ protocol, enableHttp }) { protocol.interceptFileProtocol('about', _disabledHandler); protocol.interceptFileProtocol('content', _disabledHandler); protocol.interceptFileProtocol('chrome', _disabledHandler); @@ -84,12 +84,15 @@ function installWebHandler({ protocol }) { protocol.interceptFileProtocol('filesystem', _disabledHandler); protocol.interceptFileProtocol('ftp', _disabledHandler); protocol.interceptFileProtocol('gopher', _disabledHandler); - protocol.interceptFileProtocol('http', _disabledHandler); - protocol.interceptFileProtocol('https', _disabledHandler); protocol.interceptFileProtocol('javascript', _disabledHandler); protocol.interceptFileProtocol('mailto', _disabledHandler); - protocol.interceptFileProtocol('ws', _disabledHandler); - protocol.interceptFileProtocol('wss', _disabledHandler); + + if (!enableHttp) { + protocol.interceptFileProtocol('http', _disabledHandler); + protocol.interceptFileProtocol('https', _disabledHandler); + protocol.interceptFileProtocol('ws', _disabledHandler); + protocol.interceptFileProtocol('wss', _disabledHandler); + } } module.exports = { diff --git a/js/background.js b/js/background.js index f1bf083ac..e76db24a1 100644 --- a/js/background.js +++ b/js/background.js @@ -399,6 +399,12 @@ ), }); }, + + installStickerPack: async (packId, key) => { + window.Signal.Stickers.downloadStickerPack(packId, key, { + finalStatus: 'installed', + }); + }, }; if (isIndexedDBPresent) { diff --git a/js/modules/web_api.js b/js/modules/web_api.js index 56fdd95a3..425689131 100644 --- a/js/modules/web_api.js +++ b/js/modules/web_api.js @@ -398,6 +398,7 @@ const URL_CALLS = { messages: 'v1/messages', profile: 'v1/profile', signed: 'v2/keys/signed', + getStickerPackUpload: 'v1/sticker/pack/form', }; module.exports = { @@ -457,6 +458,7 @@ function initialize({ getStickerPackManifest, makeProxiedRequest, putAttachment, + putStickers, registerKeys, registerSupportForUnauthenticatedDelivery, removeSignalingKey, @@ -865,35 +867,10 @@ function initialize({ }); } - async function getAttachment(id) { - // This is going to the CDN, not the service, so we use _outerAjax - return _outerAjax(`${cdnUrl}/attachments/${id}`, { - certificateAuthority, - proxyUrl, - responseType: 'arraybuffer', - timeout: 0, - type: 'GET', - }); - } - - async function putAttachment(encryptedBin) { - const response = await _ajax({ - call: 'attachmentId', - httpType: 'GET', - responseType: 'json', - }); - - const { - key, - credential, - acl, - algorithm, - date, - policy, - signature, - attachmentIdString, - } = response; - + function makePutParams( + { key, credential, acl, algorithm, date, policy, signature }, + encryptedBin + ) { // Note: when using the boundary string in the POST body, it needs to be prefixed by // an extra --, and the final boundary string at the end gets a -- prefix and a -- // suffix. @@ -932,17 +909,92 @@ function initialize({ contentLength ); - // This is going to the CDN, not the service, so we use _outerAjax - await _outerAjax(`${cdnUrl}/attachments/`, { - certificateAuthority, - contentType: `multipart/form-data; boundary=${boundaryString}`, + return { data, - proxyUrl, - timeout: 0, - type: 'POST', + contentType: `multipart/form-data; boundary=${boundaryString}`, headers: { 'Content-Length': contentLength, }, + }; + } + + async function putStickers( + encryptedManifest, + encryptedStickers, + onProgress + ) { + // Get manifest and sticker upload parameters + const { packId, manifest, stickers } = await _ajax({ + call: 'getStickerPackUpload', + responseType: 'json', + type: 'GET', + urlParameters: `/${encryptedStickers.length}`, + }); + + // Upload manifest + const manifestParams = makePutParams(manifest, encryptedManifest); + // This is going to the CDN, not the service, so we use _outerAjax + await _outerAjax(`${cdnUrl}/`, { + ...manifestParams, + key: 'stickers/asdfasdf/manifest.proto', + certificateAuthority, + proxyUrl, + timeout: 0, + type: 'POST', + processData: false, + }); + + // Upload stickers + await Promise.all( + stickers.map(async (s, id) => { + const stickerParams = makePutParams(s, encryptedStickers[id]); + await _outerAjax(`${cdnUrl}/`, { + ...stickerParams, + certificateAuthority, + proxyUrl, + timeout: 0, + type: 'POST', + processData: false, + }); + if (onProgress) { + onProgress(); + } + }) + ); + + // Done! + return packId; + } + + async function getAttachment(id) { + // This is going to the CDN, not the service, so we use _outerAjax + return _outerAjax(`${cdnUrl}/attachments/${id}`, { + certificateAuthority, + proxyUrl, + responseType: 'arraybuffer', + timeout: 0, + type: 'GET', + }); + } + + async function putAttachment(encryptedBin) { + const response = await _ajax({ + call: 'attachmentId', + httpType: 'GET', + responseType: 'json', + }); + + const { attachmentIdString } = response; + + const params = makePutParams(response, encryptedBin); + + // This is going to the CDN, not the service, so we use _outerAjax + await _outerAjax(`${cdnUrl}/attachments/`, { + ...params, + certificateAuthority, + proxyUrl, + timeout: 0, + type: 'POST', processData: false, }); diff --git a/main.js b/main.js index 3ac4bb779..adb80146e 100644 --- a/main.js +++ b/main.js @@ -20,6 +20,7 @@ const getRealPath = pify(fs.realpath); const { app, BrowserWindow, + dialog, ipcMain: ipc, Menu, protocol: electronProtocol, @@ -141,9 +142,11 @@ let logger; let locale; function prepareURL(pathSegments, moreKeys) { + const parsed = url.parse(path.join(...pathSegments)); + return url.format({ - pathname: path.join.apply(null, pathSegments), - protocol: 'file:', + ...parsed, + protocol: parsed.protocol || 'file:', slashes: true, query: { name: packageJson.productName, @@ -168,8 +171,10 @@ function prepareURL(pathSegments, moreKeys) { async function handleUrl(event, target) { event.preventDefault(); - const { protocol } = url.parse(target); - if (protocol === 'http:' || protocol === 'https:') { + const { protocol, hostname } = url.parse(target); + const isDevServer = config.enableHttp && hostname === 'localhost'; + // We only want to specially handle urls that aren't requesting the dev server + if ((protocol === 'http:' || protocol === 'https:') && !isDevServer) { try { await shell.openExternal(target); } catch (error) { @@ -557,6 +562,81 @@ async function showSettingsWindow() { }); } +async function getIsLinked() { + try { + const number = await sql.getItemById('number_id'); + const password = await sql.getItemById('password'); + return Boolean(number && password); + } catch (e) { + return false; + } +} + +let stickerCreatorWindow; +async function showStickerCreator() { + if (!await getIsLinked()) { + const { message } = locale.messages[ + 'StickerCreator--Authentication--error' + ]; + + dialog.showMessageBox({ + type: 'warning', + message, + }); + + return; + } + + if (stickerCreatorWindow) { + stickerCreatorWindow.show(); + return; + } + + const { x = 0, y = 0 } = windowConfig || {}; + + const options = { + x: x + 100, + y: y + 100, + width: 800, + minWidth: 800, + height: 650, + title: locale.messages.signalDesktopStickerCreator, + autoHideMenuBar: true, + backgroundColor: '#2090EA', + show: false, + webPreferences: { + nodeIntegration: false, + nodeIntegrationInWorker: false, + contextIsolation: false, + preload: path.join(__dirname, 'sticker-creator/preload.js'), + nativeWindowOpen: true, + }, + }; + + stickerCreatorWindow = new BrowserWindow(options); + + captureClicks(stickerCreatorWindow); + + const appUrl = config.enableHttp + ? prepareURL(['http://localhost:6380/sticker-creator/dist/index.html']) + : prepareURL([__dirname, 'sticker-creator/dist/index.html']); + + stickerCreatorWindow.loadURL(appUrl); + + stickerCreatorWindow.on('closed', () => { + stickerCreatorWindow = null; + }); + + stickerCreatorWindow.once('ready-to-show', () => { + stickerCreatorWindow.show(); + + if (config.get('openDevTools')) { + // Open the DevTools. + stickerCreatorWindow.webContents.openDevTools(); + } + }); +} + let debugLogWindow; async function showDebugLogWindow() { if (debugLogWindow) { @@ -672,6 +752,7 @@ app.on('ready', async () => { } installWebHandler({ + enableHttp: config.enableHttp, protocol: electronProtocol, }); @@ -778,13 +859,15 @@ app.on('ready', async () => { function setupMenu(options) { const { platform } = process; - const menuOptions = Object.assign({}, options, { + const menuOptions = { + ...options, development, showDebugLog: showDebugLogWindow, showKeyboardShortcuts, showWindow, showAbout, showSettings: showSettingsWindow, + showStickerCreator, openReleaseNotes, openNewBugForm, openSupportPage, @@ -793,7 +876,7 @@ function setupMenu(options) { setupWithImport, setupAsNewDevice, setupAsStandalone, - }); + }; const template = createTemplate(menuOptions, locale.messages); const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); @@ -1090,3 +1173,8 @@ function handleSgnlLink(incomingUrl) { console.error('Unhandled sgnl link'); } } + +ipc.on('install-sticker-pack', (_event, packId, packKeyHex) => { + const packKey = Buffer.from(packKeyHex, 'hex').toString('base64'); + mainWindow.webContents.send('install-sticker-pack', { packId, packKey }); +}); diff --git a/package.json b/package.json index 5ad137320..4dcf90199 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "grunt": "grunt", "icon-gen": "electron-icon-maker --input=images/icon_1024.png --output=./build", "generate": "yarn icon-gen && yarn grunt", - "build": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV", - "build-release": "SIGNAL_ENV=production npm run build -- --config.directories.output=release", + "build-release": "npm run build", "sign-release": "node ts/updater/generateSignature.js", "notarize": "node ts/build/notarize.js", "build-module-protobuf": "pbjs --target static-module --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js", @@ -42,11 +41,25 @@ "clean-transpile": "rimraf ts/**/*.js && rimraf ts/*.js", "open-coverage": "open coverage/lcov-report/index.html", "styleguide": "styleguidist server", - "ready": "yarn clean-transpile && yarn grunt && yarn lint && yarn test-node && yarn test-electron && yarn lint-deps" + "ready": "yarn clean-transpile && yarn grunt && yarn lint && yarn test-node && yarn test-electron && yarn lint-deps", + "dev": "run-p --print-label dev:*", + "dev:webpack": "NODE_ENV=development webpack-dev-server --hot", + "dev:typed-scss": "yarn build:typed-scss -w", + "dev:storybook": "start-storybook -p 6006 -s ./", + "build": "run-s --print-label build:grunt build:typed-scss build:webpack build:release", + "build:grunt": "yarn grunt", + "build:typed-scss": "tsm sticker-creator", + "build:webpack": "cross-env NODE_ENV=production webpack", + "build:electron": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV", + "build:release": "cross-env SIGNAL_ENV=production npm run build:electron -- --config.directories.output=release", + "preverify:ts": "yarn build:typed-scss", + "verify": "run-p --print-label verify:*", + "verify:ts": "tsc --noEmit" }, "dependencies": { "@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#00fd0f8a6623c6683280976d2a92b41d09c744bc", "@sindresorhus/is": "0.8.0", + "array-move": "2.1.0", "backbone": "1.3.3", "blob-util": "1.3.0", "blueimp-canvas-to-blob": "3.14.0", @@ -54,11 +67,11 @@ "bunyan": "1.8.12", "classnames": "2.2.5", "config": "1.28.1", + "copy-text-to-clipboard": "2.1.0", "curve25519-n": "https://github.com/scottnonnenberg-signal/node-curve25519.git#1bd0580843dcf836284dee7f1c4dfb4c698f7969", "draft-js": "0.10.5", "electron-context-menu": "0.11.0", "electron-editor-context-menu": "1.1.1", - "electron-is-dev": "0.3.0", "electron-mocha": "8.1.1", "electron-notarize": "0.1.1", "emoji-datasource": "4.1.0", @@ -73,6 +86,7 @@ "google-libphonenumber": "3.2.6", "got": "8.2.0", "he": "1.2.0", + "history": "4.9.0", "intl-tel-input": "12.1.15", "jquery": "3.4.1", "js-yaml": "3.13.1", @@ -94,22 +108,30 @@ "react": "16.8.3", "react-contextmenu": "2.11.0", "react-dom": "16.8.3", + "react-dropzone": "10.1.7", + "react-hot-loader": "4.12.11", "react-measure": "2.3.0", "react-popper": "1.3.3", - "react-redux": "6.0.1", + "react-redux": "7.1.0", + "react-router-dom": "5.0.1", + "react-sortable-hoc": "1.9.1", "react-virtualized": "9.21.0", "read-last-lines": "1.3.0", "redux": "4.0.1", "redux-logger": "3.0.6", "redux-promise-middleware": "6.1.0", + "redux-ts-utils": "3.2.2", "reselect": "4.0.0", "rimraf": "2.6.2", + "sanitize.css": "11.0.0", "semver": "5.4.1", + "sharp": "0.23.0", "spellchecker": "3.7.0", "tar": "4.4.8", "testcheck": "1.0.0-rc.2", "tmp": "0.0.33", "to-arraybuffer": "1.0.1", + "typeface-inter": "^3.10.0", "underscore": "1.9.0", "uuid": "3.3.2", "websocket": "1.0.28" @@ -118,6 +140,14 @@ "fbjs/isomorphic-fetch/node-fetch": "https://github.com/scottnonnenberg-signal/node-fetch.git#3e5f51e08c647ee5f20c43b15cf2d352d61c36b4" }, "devDependencies": { + "@babel/core": "7.5.5", + "@babel/plugin-proposal-class-properties": "^7.7.4", + "@babel/preset-react": "7.0.0", + "@babel/preset-typescript": "7.3.3", + "@storybook/addon-actions": "5.1.11", + "@storybook/addon-knobs": "5.1.11", + "@storybook/addons": "5.1.11", + "@storybook/react": "5.1.11", "@types/chai": "4.1.2", "@types/classnames": "2.2.3", "@types/config": "0.0.34", @@ -126,6 +156,8 @@ "@types/fs-extra": "5.0.5", "@types/google-libphonenumber": "7.4.14", "@types/got": "9.4.1", + "@types/history": "4.7.2", + "@types/html-webpack-plugin": "3.2.1", "@types/jquery": "3.3.29", "@types/js-yaml": "3.12.0", "@types/linkify-it": "2.0.3", @@ -138,17 +170,29 @@ "@types/react": "16.8.5", "@types/react-dom": "16.8.2", "@types/react-measure": "2.0.5", - "@types/react-redux": "7.0.1", + "@types/react-redux": "7.1.2", + "@types/react-router-dom": "4.3.4", + "@types/react-sortable-hoc": "0.6.5", "@types/react-virtualized": "9.18.12", "@types/redux-logger": "3.0.7", "@types/rimraf": "2.0.2", "@types/semver": "5.5.0", "@types/sinon": "4.3.1", + "@types/storybook__addon-actions": "3.4.3", + "@types/storybook__addon-knobs": "5.0.3", + "@types/storybook__react": "4.0.2", "@types/uuid": "3.4.4", + "@types/webpack": "4.39.0", + "@types/webpack-dev-server": "3.1.7", "arraybuffer-loader": "1.0.3", "asar": "0.14.0", + "babel-core": "7.0.0-bridge.0", + "babel-loader": "8.0.6", + "babel-plugin-lodash": "3.3.4", "bower": "1.8.2", "chai": "4.1.2", + "cross-env": "5.2.0", + "css-loader": "3.2.0", "dashdash": "1.14.1", "electron": "6.1.4", "electron-builder": "21.2.0", @@ -160,6 +204,7 @@ "eslint-plugin-mocha": "4.12.1", "eslint-plugin-more": "0.3.1", "extract-zip": "1.6.6", + "file-loader": "4.2.0", "grunt": "1.0.1", "grunt-cli": "1.2.0", "grunt-contrib-concat": "1.0.1", @@ -168,24 +213,32 @@ "grunt-exec": "3.0.0", "grunt-gitinfo": "0.1.7", "grunt-sass": "3.0.1", + "html-webpack-plugin": "3.2.0", "jsdoc": "3.6.2", "mocha": "4.1.0", "mocha-testcheck": "1.0.0-rc.0", "node-sass": "4.12.0", "node-sass-import-once": "1.2.0", + "npm-run-all": "4.1.5", "nyc": "11.4.1", "patch-package": "6.1.2", "prettier": "1.12.0", "react-docgen-typescript": "1.2.6", "react-styleguidist": "7.0.1", + "sass-loader": "7.2.0", "sinon": "4.4.2", "spectron": "5.0.0", + "style-loader": "1.0.0", "ts-loader": "4.1.0", + "ts-node": "8.3.0", "tslint": "5.13.0", - "tslint-microsoft-contrib": "6.0.0", + "tslint-microsoft-contrib": "6.2.0", "tslint-react": "3.6.0", + "typed-scss-modules": "0.0.11", "typescript": "3.3.3333", - "webpack": "4.4.1" + "webpack": "4.39.2", + "webpack-cli": "3.3.7", + "webpack-dev-server": "3.8.0" }, "engines": { "node": "12.4.0" @@ -280,6 +333,7 @@ "!js/register.js", "app/*", "preload.js", + "preload_utils.js", "about_preload.js", "settings_preload.js", "permissions_popup_preload.js", @@ -289,6 +343,8 @@ "fonts/**", "build/assets", "node_modules/**", + "sticker-creator/preload.js", + "sticker-creator/dist/**", "!node_modules/emoji-datasource/emoji_pretty.json", "!node_modules/emoji-datasource/*.png", "!node_modules/emoji-datasource-apple/emoji_pretty.json", @@ -307,6 +363,7 @@ "node_modules/socks/build/common/*.js", "node_modules/socks/build/client/*.js", "node_modules/smart-buffer/build/*.js", + "node_modules/sharp/build/**", "!node_modules/@journeyapps/sqlcipher/deps/*", "!node_modules/@journeyapps/sqlcipher/build/*", "!node_modules/@journeyapps/sqlcipher/lib/binding/node-*", diff --git a/permissions_popup_preload.js b/permissions_popup_preload.js index e3fa7086e..072406793 100644 --- a/permissions_popup_preload.js +++ b/permissions_popup_preload.js @@ -3,6 +3,7 @@ const { ipcRenderer, remote } = require('electron'); const url = require('url'); const i18n = require('./js/modules/i18n'); +const { makeGetter, makeSetter } = require('./preload_utils'); const { systemPreferences } = remote.require('electron'); @@ -43,31 +44,3 @@ window.getMediaPermissions = makeGetter('media-permissions'); window.setMediaPermissions = makeSetter('media-permissions'); window.getThemeSetting = makeGetter('theme-setting'); window.setThemeSetting = makeSetter('theme-setting'); - -function makeGetter(name) { - return () => - new Promise((resolve, reject) => { - ipcRenderer.once(`get-success-${name}`, (event, error, value) => { - if (error) { - return reject(error); - } - - return resolve(value); - }); - ipcRenderer.send(`get-${name}`); - }); -} - -function makeSetter(name) { - return value => - new Promise((resolve, reject) => { - ipcRenderer.once(`set-success-${name}`, (event, error) => { - if (error) { - return reject(error); - } - - return resolve(); - }); - ipcRenderer.send(`set-${name}`, value); - }); -} diff --git a/preload.js b/preload.js index 95919322f..b449901cc 100644 --- a/preload.js +++ b/preload.js @@ -3,6 +3,7 @@ const electron = require('electron'); const semver = require('semver'); const curve = require('curve25519-n'); +const { installGetter, installSetter } = require('./preload_utils'); const { deferredToPromise } = require('./js/modules/deferred_to_promise'); @@ -197,6 +198,14 @@ ipc.on('show-sticker-pack', (_event, info) => { } }); +ipc.on('install-sticker-pack', (_event, info) => { + const { packId, packKey } = info; + const { installStickerPack } = window.Events; + if (installStickerPack) { + installStickerPack(packId, packKey); + } +}); + ipc.on('get-ready-for-shutdown', async () => { const { shutdown } = window.Events || {}; if (!shutdown) { @@ -216,49 +225,6 @@ ipc.on('get-ready-for-shutdown', async () => { } }); -function installGetter(name, functionName) { - ipc.on(`get-${name}`, async () => { - const getFn = window.Events[functionName]; - if (!getFn) { - ipc.send( - `get-success-${name}`, - `installGetter: ${functionName} not found for event ${name}` - ); - return; - } - try { - ipc.send(`get-success-${name}`, null, await getFn()); - } catch (error) { - ipc.send( - `get-success-${name}`, - error && error.stack ? error.stack : error - ); - } - }); -} - -function installSetter(name, functionName) { - ipc.on(`set-${name}`, async (_event, value) => { - const setFn = window.Events[functionName]; - if (!setFn) { - ipc.send( - `set-success-${name}`, - `installSetter: ${functionName} not found for event ${name}` - ); - return; - } - try { - await setFn(value); - ipc.send(`set-success-${name}`); - } catch (error) { - ipc.send( - `set-success-${name}`, - error && error.stack ? error.stack : error - ); - } - }); -} - window.addSetupMenuItems = () => ipc.send('add-setup-menu-items'); window.removeSetupMenuItems = () => ipc.send('remove-setup-menu-items'); diff --git a/preload_utils.js b/preload_utils.js new file mode 100644 index 000000000..a9f142ea7 --- /dev/null +++ b/preload_utils.js @@ -0,0 +1,74 @@ +/* global window */ + +const { ipcRenderer: ipc } = require('electron'); + +exports.installGetter = function installGetter(name, functionName) { + ipc.on(`get-${name}`, async () => { + const getFn = window.Events[functionName]; + if (!getFn) { + ipc.send( + `get-success-${name}`, + `installGetter: ${functionName} not found for event ${name}` + ); + return; + } + try { + ipc.send(`get-success-${name}`, null, await getFn()); + } catch (error) { + ipc.send( + `get-success-${name}`, + error && error.stack ? error.stack : error + ); + } + }); +}; + +exports.installSetter = function installSetter(name, functionName) { + ipc.on(`set-${name}`, async (_event, value) => { + const setFn = window.Events[functionName]; + if (!setFn) { + ipc.send( + `set-success-${name}`, + `installSetter: ${functionName} not found for event ${name}` + ); + return; + } + try { + await setFn(value); + ipc.send(`set-success-${name}`); + } catch (error) { + ipc.send( + `set-success-${name}`, + error && error.stack ? error.stack : error + ); + } + }); +}; + +exports.makeGetter = function makeGetter(name) { + return () => + new Promise((resolve, reject) => { + ipc.once(`get-success-${name}`, (event, error, value) => { + if (error) { + return reject(error); + } + + return resolve(value); + }); + ipc.send(`get-${name}`); + }); +}; + +exports.makeSetter = function makeSetter(name) { + return value => + new Promise((resolve, reject) => { + ipc.once(`set-success-${name}`, (event, error) => { + if (error) { + return reject(error); + } + + return resolve(); + }); + ipc.send(`set-${name}`, value); + }); +}; diff --git a/sticker-creator/_mixins.scss b/sticker-creator/_mixins.scss new file mode 100644 index 000000000..d1e846c24 --- /dev/null +++ b/sticker-creator/_mixins.scss @@ -0,0 +1,9 @@ +@mixin light-theme() { + @content; +} + +@mixin dark-theme() { + :global(.dark-theme) & { + @content; + } +} diff --git a/sticker-creator/app/index.scss b/sticker-creator/app/index.scss new file mode 100644 index 000000000..f27813efc --- /dev/null +++ b/sticker-creator/app/index.scss @@ -0,0 +1,16 @@ +@import '../mixins'; +@import '../../stylesheets/variables'; + +.container { + display: grid; + height: 100vh; + grid-template-rows: 47px calc(100vh - 47px - 68px) 68px; + + @include light-theme() { + background-color: $color-white; + } + + @include dark-theme() { + background-color: $color-gray-90; + } +} diff --git a/sticker-creator/app/index.tsx b/sticker-creator/app/index.tsx new file mode 100644 index 000000000..7afbe05ed --- /dev/null +++ b/sticker-creator/app/index.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import { DropStage } from './stages/DropStage'; +import { EmojiStage } from './stages/EmojiStage'; +import { UploadStage } from './stages/UploadStage'; +import { MetaStage } from './stages/MetaStage'; +import { ShareStage } from './stages/ShareStage'; +import * as styles from './index.scss'; +import { PageHeader } from '../elements/PageHeader'; +import { useI18n } from '../util/i18n'; + +export const App = () => { + const i18n = useI18n(); + + return ( +
+ {i18n('StickerCreator--title')} + + + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/sticker-creator/app/stages/AppStage.scss b/sticker-creator/app/stages/AppStage.scss new file mode 100644 index 000000000..b5884166e --- /dev/null +++ b/sticker-creator/app/stages/AppStage.scss @@ -0,0 +1,41 @@ +.padded { + padding: 0 16px; +} + +.main { + composes: padded; + padding-top: 16px; + display: grid; + height: 100%; + grid-template-rows: 26px 36px calc(100% - 26px - 36px); + overflow: auto; +} + +.no-message { + composes: main; + grid-template-rows: 26px calc(100% - 26px); +} + +.empty { + composes: main; + grid-template-rows: 100%; +} + +.footer { + composes: padded; + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; +} + +.button { + margin-left: 12px; +} + +.toaster { + position: fixed; + bottom: 16px; + left: 50%; + transform: translate(-50%, 0px); +} diff --git a/sticker-creator/app/stages/AppStage.tsx b/sticker-creator/app/stages/AppStage.tsx new file mode 100644 index 000000000..34d1749b8 --- /dev/null +++ b/sticker-creator/app/stages/AppStage.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import * as styles from './AppStage.scss'; +import { history } from '../../util/history'; +import { Button } from '../../elements/Button'; +import { useI18n } from '../../util/i18n'; + +export type Props = { + readonly children: React.ReactNode; + readonly empty?: boolean; + readonly prev?: string; + readonly prevText?: string; + readonly next?: string; + readonly nextActive?: boolean; + readonly noMessage?: boolean; + readonly onNext?: () => unknown; + readonly onPrev?: () => unknown; + readonly nextText?: string; +}; + +const getClassName = ({ noMessage, empty }: Props) => { + if (noMessage) { + return styles.noMessage; + } + + if (empty) { + return styles.empty; + } + + return styles.main; +}; + +export const AppStage = (props: Props) => { + const { + children, + next, + nextActive, + nextText, + onNext, + onPrev, + prev, + prevText, + } = props; + const i18n = useI18n(); + + const handleNext = React.useCallback( + () => { + history.push(next); + }, + [next] + ); + + const handlePrev = React.useCallback( + () => { + history.push(prev); + }, + [prev] + ); + + return ( + <> +
{children}
+
+ {prev || onPrev ? ( + + ) : null} + {next || onNext ? ( + + ) : null} +
+ + ); +}; diff --git a/sticker-creator/app/stages/DropStage.scss b/sticker-creator/app/stages/DropStage.scss new file mode 100644 index 000000000..87aa540b8 --- /dev/null +++ b/sticker-creator/app/stages/DropStage.scss @@ -0,0 +1,24 @@ +.message { + max-width: 450px; +} + +.main { + flex-grow: 1; + margin-top: 16px; + display: flex; + flex-direction: column; +} + +.info { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; +} + +.sticker-grid { + display: grid; + grid-template-columns: repeat(auto-fill, 186px); + grid-gap: 8px; + justify-content: center; +} diff --git a/sticker-creator/app/stages/DropStage.tsx b/sticker-creator/app/stages/DropStage.tsx new file mode 100644 index 000000000..72f69a25a --- /dev/null +++ b/sticker-creator/app/stages/DropStage.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { AppStage } from './AppStage'; +import * as styles from './DropStage.scss'; +import * as appStyles from './AppStage.scss'; +import { H2, Text } from '../../elements/Typography'; +import { LabeledCheckbox } from '../../elements/LabeledCheckbox'; +import { Toast } from '../../elements/Toast'; +import { StickerGrid } from '../../components/StickerGrid'; +import { stickersDuck } from '../../store'; +import { useI18n } from '../../util/i18n'; + +const renderToaster = ({ + hasTooLarge, + numberAdded, + resetStatus, + i18n, +}: { + hasTooLarge: boolean; + numberAdded: number; + resetStatus: () => unknown; + i18n: ReturnType; +}) => { + if (hasTooLarge) { + return ( +
+ + {i18n('StickerCreator--Toasts--tooLarge')} + +
+ ); + } + + if (numberAdded > 0) { + return ( +
+ + {i18n('StickerCreator--Toasts--imagesAdded', [numberAdded])} + +
+ ); + } + + return null; +}; + +export const DropStage = () => { + const i18n = useI18n(); + const stickerPaths = stickersDuck.useStickerOrder(); + const stickersReady = stickersDuck.useStickersReady(); + const haveStickers = stickerPaths.length > 0; + const hasTooLarge = stickersDuck.useHasTooLarge(); + const numberAdded = stickersDuck.useImageAddedCount(); + const [showGuide, setShowGuide] = React.useState(true); + const { resetStatus } = stickersDuck.useStickerActions(); + + React.useEffect(() => { + resetStatus(); + }, []); + + return ( + +

{i18n('StickerCreator--DropStage--title')}

+
+ + {i18n('StickerCreator--DropStage--help')} + + {haveStickers ? ( + + {i18n('StickerCreator--DropStage--showMargins')} + + ) : null} +
+
+ +
+ {renderToaster({ hasTooLarge, numberAdded, resetStatus, i18n })} +
+ ); +}; diff --git a/sticker-creator/app/stages/EmojiStage.tsx b/sticker-creator/app/stages/EmojiStage.tsx new file mode 100644 index 000000000..7d3ac8f8f --- /dev/null +++ b/sticker-creator/app/stages/EmojiStage.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { AppStage } from './AppStage'; +import * as styles from './DropStage.scss'; +import { H2, Text } from '../../elements/Typography'; +import { StickerGrid } from '../../components/StickerGrid'; +import { stickersDuck } from '../../store'; +import { useI18n } from '../../util/i18n'; + +export const EmojiStage = () => { + const i18n = useI18n(); + const emojisReady = stickersDuck.useEmojisReady(); + + return ( + +

{i18n('StickerCreator--EmojiStage--title')}

+
+ + {i18n('StickerCreator--EmojiStage--help')} + +
+
+ +
+
+ ); +}; diff --git a/sticker-creator/app/stages/MetaStage.scss b/sticker-creator/app/stages/MetaStage.scss new file mode 100644 index 000000000..ad75b9581 --- /dev/null +++ b/sticker-creator/app/stages/MetaStage.scss @@ -0,0 +1,72 @@ +@import '../../../stylesheets/variables'; +@import '../../mixins'; + +.main { + display: flex; + flex-direction: column; + align-items: center; +} + +.row { + margin-bottom: 18px; + width: 448px; +} + +.cover-container { + display: flex; + flex-direction: row; + justify-content: center; + padding: 18px; +} + +.label { + user-select: none; + font-size: 13px; + font-family: $inter; + font-weight: 500; + margin: 0; + + @include light-theme() { + color: $color-gray-90; + } + + @include dark-theme() { + color: $color-white; + } +} + +.cover-image { + width: 178px; + height: 178px; +} + +.cover-frame { + composes: cover-image; + overflow: hidden; + + border: { + radius: 4px; + style: solid; + width: 1px; + } + + @include light-theme() { + border-color: $color-gray-60; + } + + @include dark-theme() { + border-color: $color-gray-25; + } +} + +.cover-frame-active { + composes: cover-frame; + + @include light-theme() { + border-color: $color-signal-blue; + } + + @include dark-theme() { + border-color: $color-signal-blue; + } +} diff --git a/sticker-creator/app/stages/MetaStage.tsx b/sticker-creator/app/stages/MetaStage.tsx new file mode 100644 index 000000000..805290c22 --- /dev/null +++ b/sticker-creator/app/stages/MetaStage.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import { FileWithPath, useDropzone } from 'react-dropzone'; +import { AppStage } from './AppStage'; +import * as styles from './MetaStage.scss'; +import { convertToWebp } from '../../util/preload'; +import { history } from '../../util/history'; +import { H2, Text } from '../../elements/Typography'; +import { LabeledInput } from '../../elements/LabeledInput'; +import { ConfirmModal } from '../../components/ConfirmModal'; +import { stickersDuck } from '../../store'; +import { useI18n } from '../../util/i18n'; + +// tslint:disable-next-line max-func-body-length +export const MetaStage = () => { + const i18n = useI18n(); + const actions = stickersDuck.useStickerActions(); + const valid = stickersDuck.useAllDataValid(); + const cover = stickersDuck.useCover(); + const title = stickersDuck.useTitle(); + const author = stickersDuck.useAuthor(); + const [confirming, setConfirming] = React.useState(false); + + const onDrop = React.useCallback( + async ([{ path }]: Array) => { + const webp = await convertToWebp(path); + actions.setCover(webp); + }, + [actions] + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + accept: ['image/png'], + }); + + const onNext = React.useCallback( + () => { + setConfirming(true); + }, + [setConfirming] + ); + + const onCancel = React.useCallback( + () => { + setConfirming(false); + }, + [setConfirming] + ); + + const onConfirm = React.useCallback( + () => { + history.push('/upload'); + }, + [setConfirming] + ); + + const coverFrameClass = isDragActive + ? styles.coverFrameActive + : styles.coverFrame; + + return ( + + {confirming ? ( + + {i18n('StickerCreator--MetaStage--ConfirmDialog--text')} + + ) : null} +

{i18n('StickerCreator--MetaStage--title')}

+
+
+ + {i18n('StickerCreator--MetaStage--Field--title')} + +
+
+ + {i18n('StickerCreator--MetaStage--Field--author')} + +
+
+

+ {i18n('StickerCreator--MetaStage--Field--cover')} +

+ {i18n('StickerCreator--MetaStage--Field--cover--help')} +
+
+ {cover.src ? ( + Cover + ) : null} + {/* tslint:disable-next-line react-a11y-input-elements */} + +
+
+
+
+
+ ); +}; diff --git a/sticker-creator/app/stages/ShareStage.scss b/sticker-creator/app/stages/ShareStage.scss new file mode 100644 index 000000000..2016a0bea --- /dev/null +++ b/sticker-creator/app/stages/ShareStage.scss @@ -0,0 +1,24 @@ +@import '../../../stylesheets/variables'; +@import '../../mixins'; + +.main { + display: flex; + flex-direction: column; + align-items: center; +} + +.message { + max-width: 450px; +} + +.call-to-action { + max-width: 500px; +} + +.row { + margin-bottom: 18px; + display: flex; + flex-direction: row; + justify-content: center; + flex-shrink: 0; +} diff --git a/sticker-creator/app/stages/ShareStage.tsx b/sticker-creator/app/stages/ShareStage.tsx new file mode 100644 index 000000000..2bf899193 --- /dev/null +++ b/sticker-creator/app/stages/ShareStage.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; +import { AppStage } from './AppStage'; +import * as styles from './ShareStage.scss'; +import * as appStyles from './AppStage.scss'; +import { history } from '../../util/history'; +import { H2, Text } from '../../elements/Typography'; +import { CopyText } from '../../elements/CopyText'; +import { Toast } from '../../elements/Toast'; +import { ShareButtons } from '../../components/ShareButtons'; +import { StickerPackPreview } from '../../components/StickerPackPreview'; +import { stickersDuck } from '../../store'; +import { useI18n } from '../../util/i18n'; +import { Intl } from '../../../ts/components/Intl'; + +export const ShareStage = () => { + const i18n = useI18n(); + const actions = stickersDuck.useStickerActions(); + const title = stickersDuck.useTitle(); + const author = stickersDuck.useAuthor(); + const images = stickersDuck.useOrderedImagePaths(); + const shareUrl = stickersDuck.usePackUrl(); + const [linkCopied, setLinkCopied] = React.useState(false); + const onCopy = React.useCallback(() => setLinkCopied(true), [setLinkCopied]); + const resetLinkCopied = React.useCallback(() => setLinkCopied(false), [ + setLinkCopied, + ]); + + const handleNext = React.useCallback(() => { + window.close(); + }, []); + + const handlePrev = React.useCallback(() => { + actions.reset(); + history.push('/'); + }, []); + + return ( + + {shareUrl ? ( + <> +

{i18n('StickerCreator--ShareStage--title')}

+ + {i18n('StickerCreator--ShareStage--help')} + +
+
+ +
+
+ +
+
+ + #makeprivacystick, + ]} + /> + +
+
+ +
+
+ {linkCopied ? ( +
+ + {i18n('StickerCreator--Toasts--linkedCopied')} + +
+ ) : null} + + ) : null} +
+ ); +}; diff --git a/sticker-creator/app/stages/UploadStage.scss b/sticker-creator/app/stages/UploadStage.scss new file mode 100644 index 000000000..da391c524 --- /dev/null +++ b/sticker-creator/app/stages/UploadStage.scss @@ -0,0 +1,10 @@ +.base { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.progress { + margin: 24px 0; +} diff --git a/sticker-creator/app/stages/UploadStage.tsx b/sticker-creator/app/stages/UploadStage.tsx new file mode 100644 index 000000000..241ce63ed --- /dev/null +++ b/sticker-creator/app/stages/UploadStage.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { noop } from 'lodash'; +import { AppStage } from './AppStage'; +import * as styles from './UploadStage.scss'; +import { history } from '../../util/history'; +import { ProgressBar } from '../../elements/ProgressBar'; +import { H2, Text } from '../../elements/Typography'; +import { Button } from '../../elements/Button'; +import { stickersDuck } from '../../store'; +import { encryptAndUpload } from '../../util/preload'; +import { useI18n } from '../../util/i18n'; + +const handleCancel = () => history.push('/add-meta'); + +export const UploadStage = () => { + const i18n = useI18n(); + const actions = stickersDuck.useStickerActions(); + const cover = stickersDuck.useCover(); + const title = stickersDuck.useTitle(); + const author = stickersDuck.useAuthor(); + const orderedData = stickersDuck.useSelectOrderedData(); + const total = orderedData.length; + const [complete, setComplete] = React.useState(0); + + React.useEffect( + () => { + (async () => { + const onProgress = () => setComplete(i => i + 1); + try { + const packMeta = await encryptAndUpload( + { title, author }, + orderedData, + cover, + onProgress + ); + actions.setPackMeta(packMeta); + history.push('/share'); + } catch (e) { + history.push('/add-meta'); + } + })(); + + return noop; + }, + [title, author, cover, orderedData] + ); + + return ( + +
+

{i18n('StickerCreator--UploadStage--title')}

+ + {i18n('StickerCreator--UploadStage-uploaded', [complete, total])} + + + +
+
+ ); +}; diff --git a/sticker-creator/components/ConfirmModal.scss b/sticker-creator/components/ConfirmModal.scss new file mode 100644 index 000000000..eed53305d --- /dev/null +++ b/sticker-creator/components/ConfirmModal.scss @@ -0,0 +1,11 @@ +.facade { + background: rgba(0, 0, 0, 0.33); + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + position: fixed; + left: 0; + top: 0; +} diff --git a/sticker-creator/components/ConfirmModal.tsx b/sticker-creator/components/ConfirmModal.tsx new file mode 100644 index 000000000..2756b44e7 --- /dev/null +++ b/sticker-creator/components/ConfirmModal.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { createPortal } from 'react-dom'; +import * as styles from './ConfirmModal.scss'; +import { ConfirmDialog, Props } from '../elements/ConfirmDialog'; + +export type Mode = 'removable' | 'pick-emoji' | 'add'; + +export const ConfirmModal = React.memo( + // tslint:disable-next-line max-func-body-length + (props: Props) => { + const { onCancel } = props; + const [popperRoot, setPopperRoot] = React.useState(); + + // Create popper root and handle outside clicks + React.useEffect( + () => { + const root = document.createElement('div'); + setPopperRoot(root); + document.body.appendChild(root); + const handleOutsideClick = ({ target }: MouseEvent) => { + if (!root.contains(target as Node)) { + onCancel(); + } + }; + document.addEventListener('click', handleOutsideClick); + + return () => { + document.body.removeChild(root); + document.removeEventListener('click', handleOutsideClick); + }; + }, + [onCancel] + ); + + return popperRoot + ? createPortal( +
+ +
, + popperRoot + ) + : null; + } +); diff --git a/sticker-creator/components/ShareButtons.scss b/sticker-creator/components/ShareButtons.scss new file mode 100644 index 000000000..95eaa8cb8 --- /dev/null +++ b/sticker-creator/components/ShareButtons.scss @@ -0,0 +1,27 @@ +@import '../../stylesheets/variables'; +@import '../mixins'; + +.container { + display: flex; + justify-content: center; +} + +.text { + @include light-theme() { + border: 1px solid $color-gray-15; + color: $color-gray-90; + } + + @include dark-theme() { + border: 1px solid $color-gray-60; + color: $color-white; + } +} + +.button { + width: 32px; + height: 32px; + background: transparent; + border: none; + margin-left: 12px; +} diff --git a/sticker-creator/components/ShareButtons.stories.tsx b/sticker-creator/components/ShareButtons.stories.tsx new file mode 100644 index 000000000..7a7bfc7c5 --- /dev/null +++ b/sticker-creator/components/ShareButtons.stories.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { StoryRow } from '../elements/StoryRow'; +import { ShareButtons } from './ShareButtons'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/components', module).add('ShareButtons', () => { + const value = text('value', 'https://signal.org'); + + return ( + + + + ); +}); diff --git a/sticker-creator/components/ShareButtons.tsx b/sticker-creator/components/ShareButtons.tsx new file mode 100644 index 000000000..791569fcf --- /dev/null +++ b/sticker-creator/components/ShareButtons.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import * as styles from './ShareButtons.scss'; +import { useI18n } from '../util/i18n'; + +export type Props = { + value: string; +}; + +export const ShareButtons = React.memo(({ value }: Props) => { + const i18n = useI18n(); + + const buttonPaths = React.useMemo>( + () => { + const packUrl = encodeURIComponent(value); + const text = encodeURIComponent( + `${i18n('StickerCreator--ShareStage--socialMessage')} ${value}` + ); + + return [ + // Facebook + [ + i18n('StickerCreator--ShareButtons--facebook'), + '#4267B2', + 'M20.155 10.656l-1.506.001c-1.181 0-1.41.561-1.41 1.384v1.816h2.817l-.367 2.845h-2.45V24h-2.937v-7.298h-2.456v-2.845h2.456V11.76c0-2.435 1.487-3.76 3.658-3.76 1.04 0 1.934.077 2.195.112v2.544z', + `https://www.facebook.com/sharer/sharer.php?u=${packUrl}`, + ], + // Twitter + [ + i18n('StickerCreator--ShareButtons--twitter'), + '#1CA1F2', + 'M22.362 12.737c.006.141.01.282.01.425 0 4.337-3.302 9.339-9.34 9.339A9.294 9.294 0 018 21.027c.257.03.518.045.783.045a6.584 6.584 0 004.077-1.405 3.285 3.285 0 01-3.067-2.279 3.312 3.312 0 001.483-.057 3.283 3.283 0 01-2.633-3.218v-.042c.442.246.949.394 1.487.411a3.282 3.282 0 01-1.016-4.383 9.32 9.32 0 006.766 3.43 3.283 3.283 0 015.593-2.994 6.568 6.568 0 002.085-.796 3.299 3.299 0 01-1.443 1.816A6.587 6.587 0 0024 11.038a6.682 6.682 0 01-1.638 1.699', + `https://twitter.com/intent/tweet?text=${text}`, + ], + // Pinterest + // [ + // i18n('StickerCreator--ShareButtons--pinterest'), + // '#BD081C', + // 'M17.234 19.563c-.992 0-1.926-.536-2.245-1.146 0 0-.534 2.118-.646 2.527-.398 1.444-1.569 2.889-1.66 3.007-.063.083-.203.057-.218-.052-.025-.184-.324-2.007.028-3.493l1.182-5.008s-.293-.587-.293-1.454c0-1.362.789-2.379 1.772-2.379.836 0 1.239.628 1.239 1.38 0 .84-.535 2.097-.811 3.261-.231.975.489 1.77 1.451 1.77 1.74 0 2.913-2.236 2.913-4.886 0-2.014-1.356-3.522-3.824-3.522-2.787 0-4.525 2.079-4.525 4.402 0 .8.237 1.365.607 1.802.17.201.194.282.132.512-.045.17-.145.576-.188.738-.061.233-.249.316-.46.23-1.283-.524-1.882-1.931-1.882-3.511C9.806 11.13 12.008 8 16.374 8c3.51 0 5.819 2.538 5.819 5.265 0 3.605-2.005 6.298-4.959 6.298', + // `https://pinterest.com/pin/create/button/?url=${packUrl}`, + // ], + // Whatsapp + [ + i18n('StickerCreator--ShareButtons--whatsapp'), + '#25D366', + 'M16.033 23.862h-.003a7.914 7.914 0 01-3.79-.965L8.035 24l1.126-4.109a7.907 7.907 0 01-1.059-3.964C8.104 11.556 11.661 8 16.033 8c2.121 0 4.113.826 5.61 2.325a7.878 7.878 0 012.321 5.609c-.002 4.371-3.56 7.928-7.931 7.928zm3.88-5.101c-.165.463-.957.885-1.338.942a2.727 2.727 0 01-1.248-.078 11.546 11.546 0 01-1.13-.418c-1.987-.858-3.286-2.859-3.385-2.991-.1-.132-.81-1.074-.81-2.049 0-.975.513-1.455.695-1.653a.728.728 0 01.528-.248c.132 0 .264.001.38.007.122.006.285-.046.446.34.165.397.56 1.372.61 1.471.05.099.083.215.017.347-.066.132-.1.215-.198.331-.1.115-.208.258-.297.347-.1.098-.203.206-.087.404.116.198.513.847 1.102 1.372.757.675 1.396.884 1.594.984.198.099.314.082.429-.05.116-.132.496-.578.628-.777.132-.198.264-.165.446-.099.18.066 1.156.545 1.354.645.198.099.33.148.38.231.049.083.049.479-.116.942zm-3.877-9.422c-3.636 0-6.594 2.956-6.595 6.589 0 1.245.348 2.458 1.008 3.507l.157.249-.666 2.432 2.495-.654.24.142a6.573 6.573 0 003.355.919h.003a6.6 6.6 0 006.592-6.59 6.55 6.55 0 00-1.93-4.662 6.549 6.549 0 00-4.66-1.932z', + `https://wa.me?text=${text}`, + ], + ]; + }, + [i18n, value] + ); + + return ( +
+ {buttonPaths.map(([title, fill, path, url]) => ( + + ))} +
+ ); +}); diff --git a/sticker-creator/components/StickerFrame.scss b/sticker-creator/components/StickerFrame.scss new file mode 100644 index 000000000..4a33feb3b --- /dev/null +++ b/sticker-creator/components/StickerFrame.scss @@ -0,0 +1,140 @@ +@import '../mixins'; +@import '../../stylesheets/variables'; + +$width: 186px; +$height: 186px; +$guide-offset: 6px; +$border-width: 1px; + +.container { + position: relative; + width: $width; + height: $height; + border: { + radius: 4px; + width: $border-width; + style: solid; + } + overflow: hidden; + user-select: none; + + @include light-theme() { + border-color: $color-gray-25; + background: $color-white; + } + + @include dark-theme() { + border-color: $color-gray-60; + background: $color-gray-90; + } +} + +.dragActive { + composes: container; + + @include light-theme() { + border-color: $color-signal-blue; + } + + @include dark-theme() { + border-color: $color-signal-blue; + } +} + +.image { + width: $width; + height: $height; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.spinner { + composes: image; + animation: spin 1s linear infinite; + display: flex; + justify-content: center; + align-items: center; + + @include light-theme() { + color: $color-gray-25; + } + + @include dark-theme() { + color: $color-gray-60; + } +} + +.guide { + width: $width - (2 * $guide-offset); + height: $height - (2 * $guide-offset); + position: absolute; + left: $guide-offset - $border-width; + top: $guide-offset - $border-width; + border: { + radius: 0px; + width: $border-width; + style: dashed; + } + pointer-events: none; + + @include light-theme() { + border-color: $color-gray-25; + } + + @include dark-theme() { + border-color: $color-gray-60; + } +} + +.close-button { + width: 16px; + height: 16px; + position: absolute; + display: flex; + justify-content: center; + align-items: center; + top: 8px; + right: 8px; + font-family: $inter; + border: none; + background: none; + padding: 0; + + &-icon { + @include light-theme() { + color: $color-black; + } + @include dark-theme() { + color: $color-white; + } + } +} + +.emoji-button { + width: 41px; + height: 28px; + position: absolute; + top: 6px; + right: 6px; + border: none; + border-radius: 13.5px; + padding: 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + @include light-theme() { + background-color: $color-gray-05; + color: $color-gray-90; + } + + @include dark-theme() { + background-color: $color-gray-75; + color: rgba(255, 255, 255, 0.75); + } +} diff --git a/sticker-creator/components/StickerFrame.stories.tsx b/sticker-creator/components/StickerFrame.stories.tsx new file mode 100644 index 000000000..1b85c0aa1 --- /dev/null +++ b/sticker-creator/components/StickerFrame.stories.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { StoryRow } from '../elements/StoryRow'; +import { StickerFrame } from './StickerFrame'; + +import { storiesOf } from '@storybook/react'; +import { boolean, select, text } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; + +storiesOf('Sticker Creator/components', module).add('StickerFrame', () => { + const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp'); + const showGuide = boolean('show guide', true); + const mode = select('mode', [null, 'removable', 'pick-emoji', 'add'], null); + const onRemove = action('onRemove'); + const onDrop = action('onDrop'); + const [skinTone, setSkinTone] = React.useState(0); + const [emoji, setEmoji] = React.useState(undefined); + + return ( + + setEmoji(e.emoji)} + onDrop={onDrop} + /> + + ); +}); diff --git a/sticker-creator/components/StickerFrame.tsx b/sticker-creator/components/StickerFrame.tsx new file mode 100644 index 000000000..89349ed5f --- /dev/null +++ b/sticker-creator/components/StickerFrame.tsx @@ -0,0 +1,271 @@ +import * as React from 'react'; +import { createPortal } from 'react-dom'; +import { SortableHandle } from 'react-sortable-hoc'; +import { noop } from 'lodash'; +import { + Manager as PopperManager, + Popper, + Reference as PopperReference, +} from 'react-popper'; +import { AddEmoji } from '../elements/icons'; +import { DropZone, Props as DropZoneProps } from '../elements/DropZone'; +import { StickerPreview } from '../elements/StickerPreview'; +import * as styles from './StickerFrame.scss'; +import { + EmojiPickDataType, + EmojiPicker, + Props as EmojiPickerProps, +} from '../../ts/components/emoji/EmojiPicker'; +import { Emoji } from '../../ts/components/emoji/Emoji'; +import { useI18n } from '../util/i18n'; + +export type Mode = 'removable' | 'pick-emoji' | 'add'; + +export type Props = Partial< + Pick +> & + Partial> & { + readonly id?: string; + readonly emojiData?: EmojiPickDataType; + readonly image?: string; + readonly mode?: Mode; + readonly showGuide?: boolean; + onPickEmoji?({ id: string, emoji: EmojiPickData }): unknown; + onRemove?(id: string): unknown; + }; + +const spinnerSvg = ( + + + +); + +const closeSvg = ( + + + +); + +const ImageHandle = SortableHandle((props: { src: string }) => ( + Sticker +)); + +export const StickerFrame = React.memo( + // tslint:disable-next-line max-func-body-length + ({ + id, + emojiData, + image, + showGuide, + mode, + onRemove, + onPickEmoji, + skinTone, + onSetSkinTone, + onDrop, + }: Props) => { + const i18n = useI18n(); + const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false); + const [ + emojiPopperRoot, + setEmojiPopperRoot, + ] = React.useState(null); + const [previewActive, setPreviewActive] = React.useState(false); + const [ + previewPopperRoot, + setPreviewPopperRoot, + ] = React.useState(null); + const timerRef = React.useRef(); + + const handleToggleEmojiPicker = React.useCallback( + () => { + setEmojiPickerOpen(open => !open); + }, + [setEmojiPickerOpen] + ); + + const handlePickEmoji = React.useCallback( + (emoji: EmojiPickDataType) => { + onPickEmoji({ id, emoji }); + setEmojiPickerOpen(false); + }, + [id, onPickEmoji, setEmojiPickerOpen] + ); + + const handleRemove = React.useCallback( + () => { + onRemove(id); + }, + [onRemove, id] + ); + + const handleMouseEnter = React.useCallback( + () => { + window.clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => { + setPreviewActive(true); + }, 500); + }, + [timerRef, setPreviewActive] + ); + + const handleMouseLeave = React.useCallback( + () => { + clearTimeout(timerRef.current); + setPreviewActive(false); + }, + [timerRef, setPreviewActive] + ); + + React.useEffect( + () => () => { + clearTimeout(timerRef.current); + }, + [timerRef] + ); + + // Create popper root and handle outside clicks + React.useEffect( + () => { + if (emojiPickerOpen) { + const root = document.createElement('div'); + setEmojiPopperRoot(root); + document.body.appendChild(root); + const handleOutsideClick = ({ target }: MouseEvent) => { + if (!root.contains(target as Node)) { + setEmojiPickerOpen(false); + } + }; + document.addEventListener('click', handleOutsideClick); + + return () => { + document.body.removeChild(root); + document.removeEventListener('click', handleOutsideClick); + }; + } + + return noop; + }, + [emojiPickerOpen, setEmojiPickerOpen, setEmojiPopperRoot] + ); + + React.useEffect( + () => { + if (mode !== 'pick-emoji' && image && previewActive) { + const root = document.createElement('div'); + setPreviewPopperRoot(root); + document.body.appendChild(root); + + return () => { + document.body.removeChild(root); + }; + } + + return noop; + }, + [mode, image, previewActive, setPreviewPopperRoot] + ); + + const [dragActive, setDragActive] = React.useState(false); + const containerClass = dragActive ? styles.dragActive : styles.container; + + return ( + + + {({ ref: rootRef }) => ( +
+ {mode !== 'add' ? ( + image ? ( + + ) : ( +
{spinnerSvg}
+ ) + ) : null} + {showGuide && mode !== 'add' ? ( +
+ ) : null} + {mode === 'add' && onDrop ? ( + + ) : null} + {mode === 'removable' ? ( + + ) : null} + {mode === 'pick-emoji' ? ( + + + {({ ref }) => ( + + )} + + {emojiPickerOpen && emojiPopperRoot + ? createPortal( + + {({ ref, style }) => ( + + )} + , + emojiPopperRoot + ) + : null} + + ) : null} + {mode !== 'pick-emoji' && + image && + previewActive && + previewPopperRoot + ? createPortal( + + {({ ref, style, arrowProps, placement }) => ( + + )} + , + previewPopperRoot + ) + : null} +
+ )} + + + ); + } +); diff --git a/sticker-creator/components/StickerGrid.scss b/sticker-creator/components/StickerGrid.scss new file mode 100644 index 000000000..46660ae28 --- /dev/null +++ b/sticker-creator/components/StickerGrid.scss @@ -0,0 +1,13 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, 186px); + grid-template-rows: repeat(auto-fill, 186px); + grid-gap: 8px; + justify-content: center; +} + +.drop { + display: flex; + flex-direction: column; + flex-grow: 1; +} diff --git a/sticker-creator/components/StickerGrid.tsx b/sticker-creator/components/StickerGrid.tsx new file mode 100644 index 000000000..460310167 --- /dev/null +++ b/sticker-creator/components/StickerGrid.tsx @@ -0,0 +1,110 @@ +import * as React from 'react'; +import * as PQueue from 'p-queue'; +import { + SortableContainer, + SortableElement, + SortEndHandler, +} from 'react-sortable-hoc'; +import * as styles from './StickerGrid.scss'; +import { Props as StickerFrameProps, StickerFrame } from './StickerFrame'; +import { stickersDuck } from '../store'; +import { DropZone, Props as DropZoneProps } from '../elements/DropZone'; +import { convertToWebp } from '../util/preload'; + +const queue = new PQueue({ concurrency: 5 }); + +const SmartStickerFrame = SortableElement( + ({ id, showGuide, mode }: StickerFrameProps) => { + const data = stickersDuck.useStickerData(id); + const actions = stickersDuck.useStickerActions(); + const image = data.webp ? data.webp.src : undefined; + + return ( + + ); + } +); + +export type Props = Pick; + +export type InnerGridProps = Props & { + ids: Array; +}; + +const InnerGrid = SortableContainer( + ({ ids, mode, showGuide }: InnerGridProps) => { + const containerClassName = ids.length > 0 ? styles.grid : styles.drop; + const frameMode = mode === 'add' ? 'removable' : 'pick-emoji'; + + const actions = stickersDuck.useStickerActions(); + + const handleDrop = React.useCallback( + async paths => { + actions.initializeStickers(paths); + paths.forEach(path => { + queue.add(async () => { + const webp = await convertToWebp(path); + actions.addWebp(webp); + }); + }); + }, + [actions] + ); + + return ( +
+ {ids.length > 0 ? ( + <> + {ids.map((p, i) => ( + + ))} + {mode === 'add' && ids.length < stickersDuck.maxStickers ? ( + + ) : null} + + ) : ( + + )} +
+ ); + } +); + +export const StickerGrid = SortableContainer((props: Props) => { + const ids = stickersDuck.useStickerOrder(); + const actions = stickersDuck.useStickerActions(); + const handleSortEnd = React.useCallback( + sortEnd => { + actions.moveSticker(sortEnd); + }, + [actions] + ); + + return ( + + ); +}); diff --git a/sticker-creator/components/StickerPackPreview.scss b/sticker-creator/components/StickerPackPreview.scss new file mode 100644 index 000000000..ff163cd63 --- /dev/null +++ b/sticker-creator/components/StickerPackPreview.scss @@ -0,0 +1,126 @@ +@import '../mixins'; +@import '../../stylesheets/variables'; + +@mixin background() { + @include light-theme() { + background: $color-white; + } + + @include dark-theme() { + background: $color-gray-75; + } +} + +.container { + position: relative; + width: 330px; + height: 270px; + border-radius: 3px; + overflow: hidden; + box-shadow: 0 3px 9px 0px rgba(0, 0, 0, 0.2); + @include background(); +} + +.title-bar { + height: 27px; + padding: 0 12px; + display: flex; + flex-direction: row; + align-items: center; + font: { + family: $inter; + size: 10.5px; + weight: 500; + } + + @include background(); + + @include light-theme { + color: $color-gray-90; + } + + @include dark-theme { + color: $color-gray-05; + } +} + +.scroller { + height: calc(100% - 27px); + padding-bottom: 57px; + overflow: auto; +} + +.grid { + display: grid; + grid-gap: 6px; + padding: 0 16px 0 12px; + grid-template-columns: repeat(4, 1fr); + overflow: auto; + justify-items: center; +} + +.sticker { + width: 72px; + height: 72px; +} + +.meta { + width: 306px; + height: 39px; + border-radius: 3px; + padding: 0 9px; + display: flex; + flex-direction: column; + justify-content: center; + position: absolute; + left: 12px; + bottom: 12px; + + @include light-theme { + background: $color-gray-05; + } + + @include dark-theme { + background: $color-gray-60; + } +} + +.text { + font-family: $inter; +} + +.meta-title { + composes: text; + height: 15px; + line-height: 15px; + font: { + size: 12px; + weight: 500; + } + + @include light-theme { + color: $color-gray-90; + } + + @include dark-theme { + color: $color-gray-05; + } +} + +.meta-author { + composes: text; + height: 14px; + line-height: 14px; + font: { + size: 10px; + weight: normal; + } + + @include light-theme { + color: $color-gray-60; + } + + @include dark-theme { + color: $color-gray-25; + } +} diff --git a/sticker-creator/components/StickerPackPreview.stories.tsx b/sticker-creator/components/StickerPackPreview.stories.tsx new file mode 100644 index 000000000..e720b7eb2 --- /dev/null +++ b/sticker-creator/components/StickerPackPreview.stories.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { StoryRow } from '../elements/StoryRow'; +import { StickerPackPreview } from './StickerPackPreview'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/components', module).add( + 'StickerPackPreview', + () => { + const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp'); + const title = text('title', 'Sticker pack title'); + const author = text('author', 'Sticker pack author'); + const images = React.useMemo(() => Array(39).fill(image), [image]); + + return ( + + + + ); + } +); diff --git a/sticker-creator/components/StickerPackPreview.tsx b/sticker-creator/components/StickerPackPreview.tsx new file mode 100644 index 000000000..0aea9deee --- /dev/null +++ b/sticker-creator/components/StickerPackPreview.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import * as styles from './StickerPackPreview.scss'; +import { useI18n } from '../util/i18n'; + +export type Props = { + images: Array; + title: string; + author: string; +}; + +export const StickerPackPreview = React.memo( + ({ images, title, author }: Props) => { + const i18n = useI18n(); + + return ( +
+
+ {i18n('StickerCreator--Preview--title')} +
+
+
+ {images.map((src, id) => ( + {src} + ))} +
+
+
+
{title}
+
{author}
+
+
+ ); + } +); diff --git a/sticker-creator/elements/Button.scss b/sticker-creator/elements/Button.scss new file mode 100644 index 000000000..499e4fd70 --- /dev/null +++ b/sticker-creator/elements/Button.scss @@ -0,0 +1,80 @@ +@import '../mixins'; +@import '../../stylesheets/variables'; + +.base { + border: none; + min-width: 80px; + height: 36px; + padding: 0 25px; + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + font-family: $inter; + font-weight: normal; + font-size: 14px; + white-space: nowrap; + + @include light-theme() { + background-color: $color-gray-05; + color: $color-gray-90; + } + + @include dark-theme() { + background-color: $color-gray-75; + color: $color-white; + } + + &:disabled { + opacity: 0.4; + } +} + +.primary { + composes: base; + + @include light-theme() { + background-color: $color-signal-blue; + color: $color-white; + } + + @include dark-theme() { + background-color: $color-signal-blue; + color: $color-white; + } +} + +.pill { + composes: base; + height: 28px; + border-radius: 15px; + padding: 0 17px; + + @include light-theme() { + color: $color-gray-90; + border: 1px solid $color-gray-90; + background: transparent; + } + + @include dark-theme() { + color: $color-white; + border: 1px solid $color-white; + background: transparent; + } +} + +.pill-primary { + composes: pill; + + @include light-theme() { + border: none; + background-color: $color-signal-blue; + color: $color-white; + } + + @include dark-theme() { + border: none; + background-color: $color-signal-blue; + color: $color-white; + } +} diff --git a/sticker-creator/elements/Button.stories.tsx b/sticker-creator/elements/Button.stories.tsx new file mode 100644 index 000000000..6e8f116ce --- /dev/null +++ b/sticker-creator/elements/Button.stories.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { Button } from './Button'; + +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('Button', () => { + const onClick = action('onClick'); + const child = text('text', 'foo bar'); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}); diff --git a/sticker-creator/elements/Button.tsx b/sticker-creator/elements/Button.tsx new file mode 100644 index 000000000..ca91946fe --- /dev/null +++ b/sticker-creator/elements/Button.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import * as classnames from 'classnames'; +import * as styles from './Button.scss'; + +export type Props = React.HTMLProps & { + className?: string; + pill?: boolean; + primary?: boolean; + children: React.ReactNode; +}; + +const getClassName = ({ primary, pill }: Props) => { + if (pill && primary) { + return styles.pillPrimary; + } + + if (pill) { + return styles.pill; + } + + if (primary) { + return styles.primary; + } + + return styles.base; +}; + +export const Button = (props: Props) => { + const { className, pill, primary, children, ...otherProps } = props; + + return ( + + ); +}; diff --git a/sticker-creator/elements/ConfirmDialog.scss b/sticker-creator/elements/ConfirmDialog.scss new file mode 100644 index 000000000..5f3147936 --- /dev/null +++ b/sticker-creator/elements/ConfirmDialog.scss @@ -0,0 +1,98 @@ +@import '../mixins'; +@import '../../stylesheets/variables'; + +.base { + width: 468px; + height: 138px; + padding: 16px 16px 8px 16px; + display: grid; + flex-direction: column; + grid-template-rows: 33px 1fr 28px; + border-radius: 8px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 8px 20px 0 rgba(0, 0, 0, 0.33); + + @include light-theme() { + background: $color-white; + } + + @include dark-theme() { + background: $color-gray-75; + } +} + +.text { + font: { + family: $inter; + size: 14px; + } + margin: 0; + + @include light-theme() { + color: $color-gray-90; + } + + @include dark-theme() { + color: $color-gray-05; + } +} + +.title { + composes: text; + font-weight: 500; + + @include light-theme() { + color: $color-gray-90; + } + + @include dark-theme() { + color: $color-white; + } +} + +.bottom { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-content: flex-end; +} + +.button { + width: 64px; + height: 28px; + border-radius: 14px; + background: transparent; + margin-left: 4px; + text-align: center; + + font: { + family: $inter; + weight: 500; + size: 13px; + } + + @include light-theme() { + color: $color-gray-60; + border-color: $color-gray-60; + } + + @include dark-theme() { + color: $color-gray-25; + border-color: $color-gray-25; + } +} + +.button-primary { + composes: button; + + @include light-theme() { + color: $color-white; + border-color: $color-signal-blue; + background: $color-signal-blue; + } + + @include dark-theme() { + color: $color-white; + border-color: $color-signal-blue; + background: $color-signal-blue; + } +} diff --git a/sticker-creator/elements/ConfirmDialog.stories.tsx b/sticker-creator/elements/ConfirmDialog.stories.tsx new file mode 100644 index 000000000..6cf76e203 --- /dev/null +++ b/sticker-creator/elements/ConfirmDialog.stories.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { ConfirmDialog } from './ConfirmDialog'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; + +storiesOf('Sticker Creator/elements', module).add('ConfirmDialog', () => { + const title = text('title', 'Foo bar banana baz?'); + const child = text( + 'text', + 'Yadda yadda yadda yadda yadda yadda foo bar banana baz.' + ); + const confirm = text('confirm', 'Upload'); + const cancel = text('cancel', 'Cancel'); + + return ( + + + {child} + + + ); +}); diff --git a/sticker-creator/elements/ConfirmDialog.tsx b/sticker-creator/elements/ConfirmDialog.tsx new file mode 100644 index 000000000..83cdc5c65 --- /dev/null +++ b/sticker-creator/elements/ConfirmDialog.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import * as styles from './ConfirmDialog.scss'; +import { useI18n } from '../util/i18n'; + +export type Props = { + readonly title: string; + readonly children: React.ReactNode; + readonly confirm: string; + readonly onConfirm: () => unknown; + readonly cancel?: string; + readonly onCancel: () => unknown; +}; + +export const ConfirmDialog = ({ + title, + children, + confirm, + cancel, + onConfirm, + onCancel, +}: Props) => { + const i18n = useI18n(); + const cancelText = cancel || i18n('StickerCreator--ConfirmDialog--cancel'); + + return ( +
+

{title}

+

{children}

+
+ + +
+
+ ); +}; diff --git a/sticker-creator/elements/CopyText.scss b/sticker-creator/elements/CopyText.scss new file mode 100644 index 000000000..451eb757b --- /dev/null +++ b/sticker-creator/elements/CopyText.scss @@ -0,0 +1,36 @@ +@import '../../stylesheets/variables'; +@import '../mixins'; + +.container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 330px; + height: 36px; +} + +.input { + width: 242px; + height: 36px; + line-height: 34px; + padding: 0 12px; + border-radius: 4px; + background-color: transparent; + font-size: 14px; + font-family: $inter; + + &::placeholder { + color: $color-gray-45; + } + + @include light-theme() { + border: 1px solid $color-gray-15; + color: $color-gray-90; + } + + @include dark-theme() { + border: 1px solid $color-gray-60; + color: $color-white; + } +} diff --git a/sticker-creator/elements/CopyText.stories.tsx b/sticker-creator/elements/CopyText.stories.tsx new file mode 100644 index 000000000..7f4dda056 --- /dev/null +++ b/sticker-creator/elements/CopyText.stories.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { CopyText } from './CopyText'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('CopyText', () => { + const label = text('label', 'foo bar'); + const value = text('value', 'foo bar'); + + return ( + + + + ); +}); diff --git a/sticker-creator/elements/CopyText.tsx b/sticker-creator/elements/CopyText.tsx new file mode 100644 index 000000000..fd2e17477 --- /dev/null +++ b/sticker-creator/elements/CopyText.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import copy from 'copy-text-to-clipboard'; +import * as styles from './CopyText.scss'; +import { Button } from './Button'; +import { useI18n } from '../util/i18n'; + +export type Props = { + value: string; + label: string; + onCopy?: () => unknown; +}; + +export const CopyText = React.memo(({ label, onCopy, value }: Props) => { + const i18n = useI18n(); + const handleClick = React.useCallback( + () => { + copy(value); + if (onCopy) { + onCopy(); + } + }, + [onCopy, value] + ); + + return ( +
+ + +
+ ); +}); diff --git a/sticker-creator/elements/DropZone.scss b/sticker-creator/elements/DropZone.scss new file mode 100644 index 000000000..b1ef44ee2 --- /dev/null +++ b/sticker-creator/elements/DropZone.scss @@ -0,0 +1,61 @@ +@import '../../stylesheets/variables'; +@import '../mixins'; + +.base { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + flex-grow: 1; + + @include light-theme() { + color: $color-gray-25; + } + + @include dark-theme() { + color: $color-gray-60; + } +} + +.text { + margin: 16px 0 0 0; + font-family: $inter; + font-size: 14px; + font-weight: normal; + + @include light-theme() { + color: $color-gray-25; + } + + @include dark-theme() { + color: $color-gray-60; + } +} + +.standalone { + composes: base; + border-radius: 4px; + border: 2px solid; + + @include light-theme() { + border-color: $color-gray-25; + } + + @include dark-theme() { + border-color: $color-gray-60; + } +} + +.active { + composes: standalone; + + @include light-theme() { + border-color: $color-signal-blue; + } + + @include dark-theme() { + border-color: $color-signal-blue; + } +} diff --git a/sticker-creator/elements/DropZone.stories.tsx b/sticker-creator/elements/DropZone.stories.tsx new file mode 100644 index 000000000..db9e94536 --- /dev/null +++ b/sticker-creator/elements/DropZone.stories.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { DropZone } from './DropZone'; + +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +storiesOf('Sticker Creator/elements', module).add('DropZone', () => { + return ; +}); diff --git a/sticker-creator/elements/DropZone.tsx b/sticker-creator/elements/DropZone.tsx new file mode 100644 index 000000000..a9ff8aaa8 --- /dev/null +++ b/sticker-creator/elements/DropZone.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { useDropzone } from 'react-dropzone'; +import * as styles from './DropZone.scss'; +import { useI18n } from '../util/i18n'; + +export type Props = { + readonly inner?: boolean; + onDrop(files: Array): unknown; + onDragActive?(active: boolean): unknown; +}; + +const getClassName = ({ inner }: Props, isDragActive: boolean) => { + if (inner) { + return styles.base; + } + + if (isDragActive) { + return styles.active; + } + + return styles.standalone; +}; + +export const DropZone = (props: Props) => { + const { inner, onDrop, onDragActive } = props; + const i18n = useI18n(); + + const handleDrop = React.useCallback( + files => { + onDrop(files.map(({ path }) => path)); + }, + [onDrop] + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop: handleDrop, + accept: ['image/png'], + }); + + React.useEffect( + () => { + if (onDragActive) { + onDragActive(isDragActive); + } + }, + [isDragActive, onDragActive] + ); + + return ( +
+ {/* tslint:disable-next-line */} + + + + + {!inner ? ( +

+ {isDragActive + ? i18n('StickerCreator--DropZone--staticText') + : i18n('StickerCreator--DropZone--activeText')} +

+ ) : null} +
+ ); +}; diff --git a/sticker-creator/elements/LabeledCheckbox.scss b/sticker-creator/elements/LabeledCheckbox.scss new file mode 100644 index 000000000..100e7c5ce --- /dev/null +++ b/sticker-creator/elements/LabeledCheckbox.scss @@ -0,0 +1,50 @@ +@import '../../stylesheets/variables'; +@import '../mixins'; + +.base { + display: flex; + flex-direction: row; + align-items: center; +} + +.input { + position: absolute; + width: 0; + height: 0; + opacity: 0; +} + +.checkbox { + width: 18px; + height: 18px; + border-radius: 2px; + display: flex; + justify-content: center; + align-items: center; + border: { + width: 2px; + style: solid; + } + + @include light-theme() { + border-color: $color-gray-60; + } + + @include dark-theme() { + border-color: $color-gray-25; + } +} + +.checkbox-checked { + composes: checkbox; + border: none; + background-color: $color-signal-blue; + color: $color-white; +} + +.label { + margin-left: 6px; + position: relative; + top: 1px; + user-select: none; +} diff --git a/sticker-creator/elements/LabeledCheckbox.stories.tsx b/sticker-creator/elements/LabeledCheckbox.stories.tsx new file mode 100644 index 000000000..af206bf1a --- /dev/null +++ b/sticker-creator/elements/LabeledCheckbox.stories.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { LabeledCheckbox } from './LabeledCheckbox'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('Labeled Checkbox', () => { + const child = text('label', 'foo bar'); + const [checked, setChecked] = React.useState(false); + + return ( + + + {child} + + + ); +}); diff --git a/sticker-creator/elements/LabeledCheckbox.tsx b/sticker-creator/elements/LabeledCheckbox.tsx new file mode 100644 index 000000000..06d89f3cc --- /dev/null +++ b/sticker-creator/elements/LabeledCheckbox.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import * as styles from './LabeledCheckbox.scss'; +import { Inline } from './Typography'; + +export type Props = { + children: React.ReactNode; + value?: boolean; + onChange?: (value: boolean) => unknown; +}; + +const checkSvg = ( + + + +); + +export const LabeledCheckbox = React.memo( + ({ children, value, onChange }: Props) => { + const handleChange = React.useCallback( + () => { + onChange(!value); + }, + [onChange, value] + ); + + const className = value ? styles.checkboxChecked : styles.checkbox; + + return ( + + ); + } +); diff --git a/sticker-creator/elements/LabeledInput.scss b/sticker-creator/elements/LabeledInput.scss new file mode 100644 index 000000000..c0b6e1a89 --- /dev/null +++ b/sticker-creator/elements/LabeledInput.scss @@ -0,0 +1,55 @@ +@import '../../stylesheets/variables'; +@import '../mixins'; + +.container { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + height: 56px; +} + +.label { + user-select: none; + font-size: 13px; + font-family: $inter; + font-weight: 500; +} + +.input { + width: 448px; + height: 34px; + line-height: 34px; + padding: 0 12px; + border-radius: 4px; + background-color: transparent; + font-size: 14px; + font-family: $inter; + + &::placeholder { + color: $color-gray-45; + } + + @include light-theme() { + border: 1px solid $color-gray-15; + color: $color-gray-90; + } + + @include dark-theme() { + border: 1px solid $color-gray-60; + color: $color-white; + } + + &:focus { + outline: none; + padding: 0 11px; + + @include light-theme() { + border: 2px solid $color-signal-blue; + } + + @include dark-theme() { + border: 2px solid $color-signal-blue; + } + } +} diff --git a/sticker-creator/elements/LabeledInput.stories.tsx b/sticker-creator/elements/LabeledInput.stories.tsx new file mode 100644 index 000000000..c8772adae --- /dev/null +++ b/sticker-creator/elements/LabeledInput.stories.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { LabeledInput } from './LabeledInput'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('LabeledInput', () => { + const child = text('label', 'foo bar'); + const placeholder = text('placeholder', 'foo bar'); + const [value, setValue] = React.useState(''); + + return ( + + + {child} + + + ); +}); diff --git a/sticker-creator/elements/LabeledInput.tsx b/sticker-creator/elements/LabeledInput.tsx new file mode 100644 index 000000000..7acc80837 --- /dev/null +++ b/sticker-creator/elements/LabeledInput.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import * as styles from './LabeledInput.scss'; +import { Inline } from './Typography'; + +export type Props = { + children: React.ReactNode; + placeholder?: string; + value?: string; + onChange?: (value: string) => unknown; +}; + +export const LabeledInput = React.memo( + ({ children, value, placeholder, onChange }: Props) => { + const handleChange = React.useCallback( + (e: React.ChangeEvent) => { + onChange(e.currentTarget.value); + }, + [onChange] + ); + + return ( + + ); + } +); diff --git a/sticker-creator/elements/MessageBubble.scss b/sticker-creator/elements/MessageBubble.scss new file mode 100644 index 000000000..75b374d44 --- /dev/null +++ b/sticker-creator/elements/MessageBubble.scss @@ -0,0 +1,13 @@ +@import '../../stylesheets/variables'; + +.base { + background-color: $color-signal-blue; + padding: 6px 12px; + border-radius: 16px; + color: $color-white-alpha-90; + font: { + size: 12px; + family: $inter; + weight: normal; + } +} diff --git a/sticker-creator/elements/MessageBubble.stories.tsx b/sticker-creator/elements/MessageBubble.stories.tsx new file mode 100644 index 000000000..a8de7c6fa --- /dev/null +++ b/sticker-creator/elements/MessageBubble.stories.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { MessageBubble } from './MessageBubble'; + +import { storiesOf } from '@storybook/react'; +import { number, text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('MessageBubble', () => { + const child = text('text', 'Foo bar banana baz'); + const minutesAgo = number('minutesAgo', 3); + + return ( + + {child} + + ); +}); diff --git a/sticker-creator/elements/MessageBubble.tsx b/sticker-creator/elements/MessageBubble.tsx new file mode 100644 index 000000000..222f84586 --- /dev/null +++ b/sticker-creator/elements/MessageBubble.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import * as styles from './MessageBubble.scss'; +import { MessageMeta, Props as MessageMetaProps } from './MessageMeta'; + +export type Props = Pick & { + children: React.ReactNode; +}; + +export const MessageBubble = ({ children, minutesAgo }: Props) => { + return ( +
+ {children} + +
+ ); +}; diff --git a/sticker-creator/elements/MessageMeta.scss b/sticker-creator/elements/MessageMeta.scss new file mode 100644 index 000000000..f601f041f --- /dev/null +++ b/sticker-creator/elements/MessageMeta.scss @@ -0,0 +1,33 @@ +@import '../mixins'; +@import '../../stylesheets/variables'; + +.base { + display: flex; + flex-direction: row; + justify-content: flex-end; + margin-top: 3px; +} + +.item { + margin-left: 6px; + font: { + size: 11px; + family: $inter; + weight: normal; + } +} + +.bubble { + composes: item; + color: rgba(255, 255, 255, 0.8); +} + +.light { + composes: item; + color: $color-gray-60; +} + +.dark { + composes: item; + color: rgba(255, 255, 255, 0.8); +} diff --git a/sticker-creator/elements/MessageMeta.tsx b/sticker-creator/elements/MessageMeta.tsx new file mode 100644 index 000000000..84f53b33e --- /dev/null +++ b/sticker-creator/elements/MessageMeta.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import * as styles from './MessageMeta.scss'; +import { useI18n } from '../util/i18n'; + +export type Props = { + kind?: 'bubble' | 'dark' | 'light'; + minutesAgo: number; +}; + +const getItemClass = ({ kind }: Props) => { + if (kind === 'dark') { + return styles.dark; + } + + if (kind === 'light') { + return styles.light; + } + + return styles.bubble; +}; + +export const MessageMeta = React.memo((props: Props) => { + const i18n = useI18n(); + const itemClass = getItemClass(props); + + return ( +
+ + + + + + +
{i18n('minutesAgo', [props.minutesAgo])}
+ + + + + + + + + + + + + + +
+ ); +}); diff --git a/sticker-creator/elements/MessageSticker.scss b/sticker-creator/elements/MessageSticker.scss new file mode 100644 index 000000000..efe77cf38 --- /dev/null +++ b/sticker-creator/elements/MessageSticker.scss @@ -0,0 +1,10 @@ +@import '../../stylesheets/variables'; + +.base { + padding: 6px 12px; +} + +.image { + width: 116px; + height: 116px; +} diff --git a/sticker-creator/elements/MessageSticker.stories.tsx b/sticker-creator/elements/MessageSticker.stories.tsx new file mode 100644 index 000000000..5c1236e79 --- /dev/null +++ b/sticker-creator/elements/MessageSticker.stories.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { MessageSticker } from './MessageSticker'; + +import { storiesOf } from '@storybook/react'; +import { number, text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('MessageSticker', () => { + const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp'); + const minutesAgo = number('minutesAgo', 3); + + return ( + + + + ); +}); diff --git a/sticker-creator/elements/MessageSticker.tsx b/sticker-creator/elements/MessageSticker.tsx new file mode 100644 index 000000000..63e4e995d --- /dev/null +++ b/sticker-creator/elements/MessageSticker.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import * as styles from './MessageSticker.scss'; +import { MessageMeta, Props as MessageMetaProps } from './MessageMeta'; + +export type Props = MessageMetaProps & { + image: string; +}; + +export const MessageSticker = ({ image, kind, minutesAgo }: Props) => { + return ( +
+ Sticker + +
+ ); +}; diff --git a/sticker-creator/elements/PageHeader.scss b/sticker-creator/elements/PageHeader.scss new file mode 100644 index 000000000..f9d6c2242 --- /dev/null +++ b/sticker-creator/elements/PageHeader.scss @@ -0,0 +1,22 @@ +@import '../../stylesheets/variables'; +@import '../mixins'; + +.base { + height: 47px; + width: 100%; + border-bottom-width: 1px; + border-bottom-style: solid; + padding: 0 16px; + display: flex; + flex-direction: row; + align-items: center; + flex-shrink: 0; + + @include light-theme() { + border-bottom-color: $color-gray-15; + } + + @include dark-theme() { + border-bottom-color: $color-gray-75; + } +} diff --git a/sticker-creator/elements/PageHeader.stories.tsx b/sticker-creator/elements/PageHeader.stories.tsx new file mode 100644 index 000000000..ee8346d05 --- /dev/null +++ b/sticker-creator/elements/PageHeader.stories.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { PageHeader } from './PageHeader'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('PageHeader', () => { + const child = text('text', 'foo bar'); + + return ( + + {child} + + ); +}); diff --git a/sticker-creator/elements/PageHeader.tsx b/sticker-creator/elements/PageHeader.tsx new file mode 100644 index 000000000..64b8c1bb0 --- /dev/null +++ b/sticker-creator/elements/PageHeader.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import * as styles from './PageHeader.scss'; +import { H1 } from './Typography'; + +export type Props = { + children: React.ReactNode; +}; + +export const PageHeader = React.memo(({ children }: Props) => ( +

{children}

+)); diff --git a/sticker-creator/elements/ProgressBar.scss b/sticker-creator/elements/ProgressBar.scss new file mode 100644 index 000000000..a04b6b945 --- /dev/null +++ b/sticker-creator/elements/ProgressBar.scss @@ -0,0 +1,24 @@ +@import '../../stylesheets/variables'; +@import '../mixins'; + +.base { + height: 4px; + width: 100%; + max-width: 448px; + + @include light-theme() { + background: $color-gray-15; + } + + @include dark-theme() { + background: $color-gray-75; + } +} + +.bar { + height: 4px; + width: 0px; + background: $color-signal-blue; + + transition: width 100ms ease-out; +} diff --git a/sticker-creator/elements/ProgressBar.stories.tsx b/sticker-creator/elements/ProgressBar.stories.tsx new file mode 100644 index 000000000..4a7c6932e --- /dev/null +++ b/sticker-creator/elements/ProgressBar.stories.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { ProgressBar } from './ProgressBar'; + +import { storiesOf } from '@storybook/react'; +import { number } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('ProgressBar', () => { + const count = number('count', 5); + const total = number('total', 10); + + return ( + + + + ); +}); diff --git a/sticker-creator/elements/ProgressBar.tsx b/sticker-creator/elements/ProgressBar.tsx new file mode 100644 index 000000000..cc99a582a --- /dev/null +++ b/sticker-creator/elements/ProgressBar.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import * as classnames from 'classnames'; +import * as styles from './ProgressBar.scss'; + +export type Props = Pick, 'className'> & { + readonly count: number; + readonly total: number; +}; + +export const ProgressBar = React.memo(({ className, count, total }: Props) => ( +
+
+
+)); diff --git a/sticker-creator/elements/StickerPreview.scss b/sticker-creator/elements/StickerPreview.scss new file mode 100644 index 000000000..5e5e42700 --- /dev/null +++ b/sticker-creator/elements/StickerPreview.scss @@ -0,0 +1,120 @@ +@import '../mixins'; +@import '../../stylesheets/variables'; + +.base { + width: 380px; + padding: 4px; + display: flex; + flex-direction: row; + border-radius: 8px; + box-shadow: 0 2px 13px 3px rgba(0, 0, 0, 0.3); + position: relative; + + @include light-theme() { + background: $color-white; + } + + @include dark-theme() { + background: $color-gray-75; + } +} + +.frame { + width: 50%; + padding: 12px 12px 3px 12px; +} + +.frame-light { + composes: frame; + background: $color-white; + border-radius: 6px 0 0 6px; +} + +.frame-dark { + composes: frame; + background: $color-black; + border-radius: 0 6px 6px 0; +} + +.bottom { + composes: base; + margin-top: 8px; +} + +.top { + composes: base; + margin-bottom: 8px; +} + +.left { + composes: base; + margin-right: 8px; +} + +.right { + composes: base; + margin-left: 8px; +} + +.arrow { + position: absolute; + width: 0; + height: 0; + border-style: solid; +} + +.arrow-top { + composes: arrow; + border-width: 0 8px 8px 8px; + top: -8px; + + @include light-theme() { + border-color: transparent transparent $color-white transparent; + } + + @include dark-theme() { + border-color: transparent transparent $color-gray-75 transparent; + } +} + +.arrow-bottom { + composes: arrow; + border-width: 8px 8px 0 8px; + bottom: -8px; + + @include light-theme() { + border-color: $color-white transparent transparent transparent; + } + + @include dark-theme() { + border-color: $color-gray-75 transparent transparent transparent; + } +} + +.arrow-left { + composes: arrow; + border-width: 8px 8px 8px 0; + left: -8px; + + @include light-theme() { + border-color: transparent $color-white transparent transparent; + } + + @include dark-theme() { + border-color: transparent $color-gray-75 transparent transparent; + } +} + +.arrow-right { + composes: arrow; + border-width: 8px 0 8px 8px; + right: -8px; + + @include light-theme() { + border-color: transparent transparent transparent $color-white; + } + + @include dark-theme() { + border-color: transparent transparent transparent $color-gray-75; + } +} diff --git a/sticker-creator/elements/StickerPreview.stories.tsx b/sticker-creator/elements/StickerPreview.stories.tsx new file mode 100644 index 000000000..e87568a09 --- /dev/null +++ b/sticker-creator/elements/StickerPreview.stories.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { StickerPreview } from './StickerPreview'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('StickerPreview', () => { + const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp'); + + return ( + + + + ); +}); diff --git a/sticker-creator/elements/StickerPreview.tsx b/sticker-creator/elements/StickerPreview.tsx new file mode 100644 index 000000000..186166701 --- /dev/null +++ b/sticker-creator/elements/StickerPreview.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { PopperArrowProps } from 'react-popper'; +import { Placement } from 'popper.js'; +import * as styles from './StickerPreview.scss'; +import { MessageBubble } from './MessageBubble'; +import { MessageSticker, Props as MessageStickerProps } from './MessageSticker'; +import { useI18n } from '../util/i18n'; + +export type Props = Pick, 'style'> & { + image: string; + arrowProps?: PopperArrowProps; + placement?: Placement; +}; + +const renderMessages = ( + text: string, + image: string, + kind: MessageStickerProps['kind'] +) => ( + <> + {text} + + +); + +const getBaseClass = (placement?: Placement) => { + if (placement === 'top') { + return styles.top; + } + + if (placement === 'right') { + return styles.right; + } + + if (placement === 'left') { + return styles.left; + } + + return styles.bottom; +}; + +const getArrowClass = (placement?: Placement) => { + if (placement === 'top') { + return styles.arrowBottom; + } + + if (placement === 'right') { + return styles.arrowLeft; + } + + if (placement === 'left') { + return styles.arrowRight; + } + + return styles.arrowTop; +}; + +export const StickerPreview = React.memo( + React.forwardRef( + ({ image, style, arrowProps, placement }: Props, ref) => { + const i18n = useI18n(); + + return ( +
+ {arrowProps ? ( +
+ ) : null} +
+ {renderMessages( + i18n('StickerCreator--StickerPreview--light'), + image, + 'light' + )} +
+
+ {renderMessages( + i18n('StickerCreator--StickerPreview--dark'), + image, + 'dark' + )} +
+
+ ); + } + ) +); diff --git a/sticker-creator/elements/StoryRow.scss b/sticker-creator/elements/StoryRow.scss new file mode 100644 index 000000000..3b24875cb --- /dev/null +++ b/sticker-creator/elements/StoryRow.scss @@ -0,0 +1,28 @@ +.base { + flex: 1; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; +} + +.left { + composes: base; + justify-content: flex-start; +} + +.right { + composes: base; + justify-content: flex-end; +} + +.top { + composes: base; + align-items: flex-start; +} + +.bottom { + composes: base; + align-items: flex-end; +} diff --git a/sticker-creator/elements/StoryRow.tsx b/sticker-creator/elements/StoryRow.tsx new file mode 100644 index 000000000..8ee8e185a --- /dev/null +++ b/sticker-creator/elements/StoryRow.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import * as styles from './StoryRow.scss'; + +export type Props = { + children: React.ReactChild; + left?: boolean; + right?: boolean; + top?: boolean; + bottom?: boolean; +}; + +const getClassName = ({ left, right, top, bottom }: Props) => { + if (left) { + return styles.left; + } + + if (right) { + return styles.right; + } + + if (top) { + return styles.top; + } + + if (bottom) { + return styles.bottom; + } + + return styles.base; +}; + +export const StoryRow = (props: Props) => ( +
{props.children}
+); diff --git a/sticker-creator/elements/Toast.scss b/sticker-creator/elements/Toast.scss new file mode 100644 index 000000000..87af83d7e --- /dev/null +++ b/sticker-creator/elements/Toast.scss @@ -0,0 +1,14 @@ +@import '../../stylesheets/variables'; + +.base { + padding: 8px 12px; + border-radius: 4px; + border: none; + background-color: $color-gray-75; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 8px 20px 0px rgba(0, 0, 0, 0.33); + font-family: $inter; + font-weight: normal; + font-size: 14px; + color: $color-gray-05; + line-height: 18px; +} diff --git a/sticker-creator/elements/Toast.stories.tsx b/sticker-creator/elements/Toast.stories.tsx new file mode 100644 index 000000000..d4cefad11 --- /dev/null +++ b/sticker-creator/elements/Toast.stories.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { Toast } from './Toast'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('Toast', () => { + const child = text('text', 'foo bar'); + + return ( + + {child} + + ); +}); diff --git a/sticker-creator/elements/Toast.tsx b/sticker-creator/elements/Toast.tsx new file mode 100644 index 000000000..c3bbf6d3d --- /dev/null +++ b/sticker-creator/elements/Toast.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import * as styles from './Toast.scss'; + +export type Props = React.HTMLProps & { + children: React.ReactNode; +}; + +export const Toast = React.memo(({ children, ...rest }: Props) => ( + +)); diff --git a/sticker-creator/elements/Typography.scss b/sticker-creator/elements/Typography.scss new file mode 100644 index 000000000..e9eba5d8a --- /dev/null +++ b/sticker-creator/elements/Typography.scss @@ -0,0 +1,65 @@ +@import '../../stylesheets/variables'; +@import '../mixins'; + +.base { + font-family: $inter; + margin: 0; +} + +.heading { + composes: base; + font-weight: 500; +} + +.h1 { + composes: heading; + font-size: 16px; + line-height: 20px; + + @include light-theme() { + color: $color-gray-90; + } + + @include dark-theme() { + color: $color-gray-05; + } +} + +.h2 { + composes: heading; + font-size: 14px; + line-height: 18px; + margin-bottom: 8px; + + @include light-theme() { + color: $color-gray-90; + } + + @include dark-theme() { + color: $color-white; + } +} + +.text { + composes: base; + font-size: 13px; + line-height: 18px; + + @include light-theme() { + color: $color-gray-90; + } + + @include dark-theme() { + color: $color-white; + } + + a { + color: $color-signal-blue; + text-decoration: none; + } +} + +.text-center { + composes: text; + text-align: center; +} diff --git a/sticker-creator/elements/Typography.stories.tsx b/sticker-creator/elements/Typography.stories.tsx new file mode 100644 index 000000000..434ac2b87 --- /dev/null +++ b/sticker-creator/elements/Typography.stories.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { StoryRow } from './StoryRow'; +import { H1, H2, Text } from './Typography'; + +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +storiesOf('Sticker Creator/elements', module).add('Typography', () => { + const child = text('text', 'foo bar'); + + return ( + <> + +

{child}

+
+ +

{child}

+
+ + + {child} {child} {child} {child} + + + + + {child} {child} {child} {child}{' '} + + Something something something dark side. + + + + + ); +}); diff --git a/sticker-creator/elements/Typography.tsx b/sticker-creator/elements/Typography.tsx new file mode 100644 index 000000000..f4416feb3 --- /dev/null +++ b/sticker-creator/elements/Typography.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import * as classnames from 'classnames'; +import * as styles from './Typography.scss'; + +export type Props = { + children: React.ReactNode; +}; + +export type HeadingProps = React.HTMLProps; +export type ParagraphProps = React.HTMLProps & { + center?: boolean; + wide?: boolean; +}; +export type SpanProps = React.HTMLProps; + +export const H1 = React.memo( + ({ children, className, ...rest }: Props & HeadingProps) => ( +

+ {children} +

+ ) +); + +export const H2 = React.memo( + ({ children, className, ...rest }: Props & HeadingProps) => ( +

+ {children} +

+ ) +); + +export const Text = React.memo( + ({ children, className, center, wide, ...rest }: Props & ParagraphProps) => ( +

+ {children} +

+ ) +); + +export const Inline = React.memo( + ({ children, className, ...rest }: Props & SpanProps) => ( + + {children} + + ) +); diff --git a/sticker-creator/elements/icons/AddEmoji.tsx b/sticker-creator/elements/icons/AddEmoji.tsx new file mode 100644 index 000000000..6a9fd052c --- /dev/null +++ b/sticker-creator/elements/icons/AddEmoji.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; + +export const AddEmoji = React.memo(() => ( + + + +)); diff --git a/sticker-creator/elements/icons/index.tsx b/sticker-creator/elements/icons/index.tsx new file mode 100644 index 000000000..42a6dc5b3 --- /dev/null +++ b/sticker-creator/elements/icons/index.tsx @@ -0,0 +1 @@ +export { AddEmoji } from './AddEmoji'; diff --git a/sticker-creator/index.html b/sticker-creator/index.html new file mode 100644 index 000000000..5a9c9414f --- /dev/null +++ b/sticker-creator/index.html @@ -0,0 +1,13 @@ + + + + + + +
+ + + + + + diff --git a/sticker-creator/index.tsx b/sticker-creator/index.tsx new file mode 100644 index 000000000..79f458b1c --- /dev/null +++ b/sticker-creator/index.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { render } from 'react-dom'; +import { Root } from './root'; +import { preloadImages } from '../ts/components/emoji/lib'; + +const root = document.getElementById('root'); + +render(, root); + +preloadImages(); diff --git a/sticker-creator/preload.js b/sticker-creator/preload.js new file mode 100644 index 000000000..c0959c771 --- /dev/null +++ b/sticker-creator/preload.js @@ -0,0 +1,159 @@ +/* global window */ +const { ipcRenderer: ipc, remote } = require('electron'); +const sharp = require('sharp'); +const pify = require('pify'); +const { readFile } = require('fs'); +const config = require('url').parse(window.location.toString(), true).query; +const { noop, uniqBy } = require('lodash'); +const { deriveStickerPackKey } = require('../js/modules/crypto'); +const { makeGetter } = require('../preload_utils'); + +const { dialog } = remote; +const { systemPreferences } = remote.require('electron'); + +window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/'; +window.PROTO_ROOT = '../../protos'; +window.getEnvironment = () => config.environment; +window.getVersion = () => config.version; +window.getGuid = require('uuid/v4'); + +window.localeMessages = ipc.sendSync('locale-data'); + +require('../js/logging'); +const Signal = require('../js/modules/signal'); + +window.Signal = Signal.setup({}); + +const { initialize: initializeWebAPI } = require('../js/modules/web_api'); + +const WebAPI = initializeWebAPI({ + url: config.serverUrl, + cdnUrl: config.cdnUrl, + certificateAuthority: config.certificateAuthority, + contentProxyUrl: config.contentProxyUrl, + proxyUrl: config.proxyUrl, +}); + +window.convertToWebp = async (path, width = 512, height = 512) => { + const pngBuffer = await pify(readFile)(path); + const buffer = await sharp(pngBuffer) + .resize({ + width, + height, + fit: 'contain', + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .webp() + .toBuffer(); + + return { + path, + buffer, + src: `data:image/webp;base64,${buffer.toString('base64')}`, + }; +}; + +window.encryptAndUpload = async ( + manifest, + stickers, + cover, + onProgress = noop +) => { + const usernameItem = await window.Signal.Data.getItemById('number_id'); + const passwordItem = await window.Signal.Data.getItemById('password'); + + if (!usernameItem || !passwordItem) { + const { message } = window.localeMessages[ + 'StickerCreator--Authentication--error' + ]; + + dialog.showMessageBox({ + type: 'warning', + message, + }); + + throw new Error(message); + } + + const { value: username } = usernameItem; + const { value: password } = passwordItem; + + const packKey = window.libsignal.crypto.getRandomBytes(32); + const encryptionKey = await deriveStickerPackKey(packKey); + const iv = window.libsignal.crypto.getRandomBytes(16); + + const server = WebAPI.connect({ username, password }); + + const uniqueStickers = uniqBy([...stickers, { webp: cover }], 'webp'); + + const manifestProto = new window.textsecure.protobuf.StickerPack(); + manifestProto.title = manifest.title; + manifestProto.author = manifest.author; + manifestProto.stickers = stickers.map(({ emoji }, id) => { + const s = new window.textsecure.protobuf.StickerPack.Sticker(); + s.id = id; + s.emoji = emoji; + + return s; + }); + const coverSticker = new window.textsecure.protobuf.StickerPack.Sticker(); + coverSticker.id = + uniqueStickers.length === stickers.length ? 0 : uniqueStickers.length - 1; + coverSticker.emoji = ''; + manifestProto.cover = coverSticker; + + const encryptedManifest = await encrypt( + manifestProto.toArrayBuffer(), + encryptionKey, + iv + ); + const encryptedStickers = await Promise.all( + uniqueStickers.map(({ webp }) => encrypt(webp.buffer, encryptionKey, iv)) + ); + + const packId = await server.putStickers( + encryptedManifest, + encryptedStickers, + onProgress + ); + + const hexKey = window.Signal.Crypto.hexFromBytes(packKey); + + ipc.send('install-sticker-pack', packId, hexKey); + + return { packId, key: hexKey }; +}; + +async function encrypt(data, key, iv) { + const { ciphertext } = await window.textsecure.crypto.encryptAttachment( + // Convert Node Buffer to ArrayBuffer + window.Signal.Crypto.concatenateBytes(data), + key, + iv + ); + + return ciphertext; +} + +const getThemeSetting = makeGetter('theme-setting'); + +async function resolveTheme() { + const theme = (await getThemeSetting()) || 'light'; + if (process.platform === 'darwin' && theme === 'system') { + return systemPreferences.isDarkMode() ? 'dark' : 'light'; + } + return theme; +} + +async function applyTheme() { + window.document.body.classList.remove('dark-theme'); + window.document.body.classList.remove('light-theme'); + window.document.body.classList.add(`${await resolveTheme()}-theme`); +} + +window.addEventListener('DOMContentLoaded', applyTheme); + +systemPreferences.subscribeNotification( + 'AppleInterfaceThemeChangedNotification', + applyTheme +); diff --git a/sticker-creator/root.tsx b/sticker-creator/root.tsx new file mode 100644 index 000000000..d31aa2e00 --- /dev/null +++ b/sticker-creator/root.tsx @@ -0,0 +1,24 @@ +// tslint:disable-next-line no-submodule-imports +import { hot } from 'react-hot-loader/root'; +import * as React from 'react'; +import { Provider as ReduxProvider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import { App } from './app'; +import { history } from './util/history'; +import { store } from './store'; +import { I18n } from './util/i18n'; + +// @ts-ignore +const { localeMessages } = window; + +const ColdRoot = () => ( + + + + + + + +); + +export const Root = hot(ColdRoot); diff --git a/sticker-creator/store/ducks/stickers.ts b/sticker-creator/store/ducks/stickers.ts new file mode 100644 index 000000000..ff7068bfc --- /dev/null +++ b/sticker-creator/store/ducks/stickers.ts @@ -0,0 +1,250 @@ +// tslint:disable no-dynamic-delete + +import { useMemo } from 'react'; +import { + createAction, + Draft, + handleAction, + reduceReducers, +} from 'redux-ts-utils'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import { clamp, pull, take, uniq } from 'lodash'; +import { SortEnd } from 'react-sortable-hoc'; +import arrayMove from 'array-move'; +import { AppState } from '../reducer'; +import { PackMetaData, WebpData } from '../../util/preload'; +import { EmojiPickDataType } from '../../../ts/components/emoji/EmojiPicker'; +import { convertShortName } from '../../../ts/components/emoji/lib'; + +export const initializeStickers = createAction>( + 'stickers/initializeStickers' +); +export const addWebp = createAction('stickers/addSticker'); +export const removeSticker = createAction('stickers/removeSticker'); +export const moveSticker = createAction('stickers/moveSticker'); +export const setCover = createAction('stickers/setCover'); +export const setEmoji = createAction<{ id: string; emoji: EmojiPickDataType }>( + 'stickers/setEmoji' +); +export const setTitle = createAction('stickers/setTitle'); +export const setAuthor = createAction('stickers/setAuthor'); +export const setPackMeta = createAction('stickers/setPackMeta'); +export const resetStatus = createAction('stickers/resetStatus'); +export const reset = createAction('stickers/reset'); + +export const minStickers = 4; +export const maxStickers = 40; +export const maxByteSize = 100 * 1024; + +export type State = { + readonly order: Array; + readonly cover?: WebpData; + readonly title: string; + readonly author: string; + readonly packId: string; + readonly packKey: string; + readonly tooLarge: number; + readonly imagesAdded: number; + readonly data: { + readonly [src: string]: { + readonly webp?: WebpData; + readonly emoji?: EmojiPickDataType; + }; + }; +}; + +const defaultState: State = { + order: [], + data: {}, + title: '', + author: '', + packId: '', + packKey: '', + tooLarge: 0, + imagesAdded: 0, +}; + +const adjustCover = (state: Draft) => { + const first = state.order[0]; + + if (first) { + state.cover = state.data[first].webp; + } else { + delete state.cover; + } +}; + +export const reducer = reduceReducers( + [ + handleAction(initializeStickers, (state, { payload }) => { + const truncated = take( + uniq([...state.order, ...payload]), + maxStickers - state.order.length + ); + truncated.forEach(path => { + if (!state.data[path]) { + state.data[path] = {}; + state.order.push(path); + } + }); + }), + + handleAction(addWebp, (state, { payload }) => { + if (payload.buffer.byteLength > maxByteSize) { + state.tooLarge = clamp(state.tooLarge + 1, 0, state.order.length); + pull(state.order, payload.path); + delete state.data[payload.path]; + } else { + const data = state.data[payload.path]; + + if (data) { + data.webp = payload; + state.imagesAdded = clamp( + state.imagesAdded + 1, + 0, + state.order.length + ); + } + } + + adjustCover(state); + }), + + handleAction(removeSticker, (state, { payload }) => { + pull(state.order, payload); + delete state.data[payload]; + adjustCover(state); + state.imagesAdded = clamp(state.imagesAdded - 1, 0, state.order.length); + }), + + handleAction(moveSticker, (state, { payload }) => { + arrayMove.mutate(state.order, payload.oldIndex, payload.newIndex); + }), + + handleAction(setCover, (state, { payload }) => { + state.cover = payload; + }), + + handleAction(setEmoji, (state, { payload }) => { + const data = state.data[payload.id]; + if (data) { + data.emoji = payload.emoji; + } + }), + + handleAction(setTitle, (state, { payload }) => { + state.title = payload; + }), + + handleAction(setAuthor, (state, { payload }) => { + state.author = payload; + }), + + handleAction(setPackMeta, (state, { payload: { packId, key } }) => { + state.packId = packId; + state.packKey = key; + }), + + handleAction(resetStatus, state => { + state.tooLarge = 0; + state.imagesAdded = 0; + }), + + handleAction(reset, () => defaultState), + ], + defaultState +); + +export const useTitle = () => + useSelector(({ stickers }: AppState) => stickers.title); +export const useAuthor = () => + useSelector(({ stickers }: AppState) => stickers.author); + +export const useCover = () => + useSelector(({ stickers }: AppState) => stickers.cover); + +export const useStickerOrder = () => + useSelector(({ stickers }: AppState) => stickers.order); + +export const useStickerData = (src: string) => + useSelector(({ stickers }: AppState) => stickers.data[src]); + +export const useStickersReady = () => + useSelector( + ({ stickers }: AppState) => + stickers.order.length >= minStickers && + stickers.order.length <= maxStickers && + Object.values(stickers.data).every(({ webp }) => !!webp) + ); + +export const useEmojisReady = () => + useSelector(({ stickers }: AppState) => + Object.values(stickers.data).every(({ emoji }) => !!emoji) + ); + +export const useAllDataValid = () => { + const stickersReady = useStickersReady(); + const emojisReady = useEmojisReady(); + const cover = useCover(); + const title = useTitle(); + const author = useAuthor(); + + return !!(stickersReady && emojisReady && cover && title && author); +}; + +const selectUrl = createSelector( + ({ stickers }: AppState) => stickers.packId, + ({ stickers }: AppState) => stickers.packKey, + (id, key) => `https://signal.art/addstickers/#pack_id=${id}&pack_key=${key}` +); + +export const usePackUrl = () => useSelector(selectUrl); +export const useHasTooLarge = () => + useSelector(({ stickers }: AppState) => stickers.tooLarge > 0); +export const useImageAddedCount = () => + useSelector(({ stickers }: AppState) => stickers.imagesAdded); + +const selectOrderedData = createSelector( + ({ stickers }: AppState) => stickers.order, + ({ stickers }) => stickers.data, + (order, data) => + order.map(id => ({ + ...data[id], + emoji: convertShortName( + data[id].emoji.shortName, + data[id].emoji.skinTone + ), + })) +); + +export const useSelectOrderedData = () => useSelector(selectOrderedData); + +const selectOrderedImagePaths = createSelector(selectOrderedData, data => + data.map(({ webp }) => webp.src) +); + +export const useOrderedImagePaths = () => useSelector(selectOrderedImagePaths); + +export const useStickerActions = () => { + const dispatch = useDispatch(); + + return useMemo( + () => ({ + addWebp: (data: WebpData) => dispatch(addWebp(data)), + initializeStickers: (paths: Array) => + dispatch(initializeStickers(paths)), + removeSticker: (src: string) => dispatch(removeSticker(src)), + moveSticker: (sortEnd: SortEnd) => dispatch(moveSticker(sortEnd)), + setCover: (webp: WebpData) => dispatch(setCover(webp)), + setEmoji: (p: { id: string; emoji: EmojiPickDataType }) => + dispatch(setEmoji(p)), + setTitle: (title: string) => dispatch(setTitle(title)), + setAuthor: (author: string) => dispatch(setAuthor(author)), + setPackMeta: (e: PackMetaData) => dispatch(setPackMeta(e)), + reset: () => dispatch(reset()), + resetStatus: () => dispatch(resetStatus()), + }), + [dispatch] + ); +}; diff --git a/sticker-creator/store/index.ts b/sticker-creator/store/index.ts new file mode 100644 index 000000000..9298f2897 --- /dev/null +++ b/sticker-creator/store/index.ts @@ -0,0 +1,8 @@ +import { createStore } from 'redux'; +import { reducer } from './reducer'; + +import * as stickersDuck from './ducks/stickers'; + +export { stickersDuck }; + +export const store = createStore(reducer); diff --git a/sticker-creator/store/reducer.ts b/sticker-creator/store/reducer.ts new file mode 100644 index 000000000..b075bec06 --- /dev/null +++ b/sticker-creator/store/reducer.ts @@ -0,0 +1,8 @@ +import { combineReducers, Reducer } from 'redux'; +import { reducer as stickers } from './ducks/stickers'; + +export const reducer = combineReducers({ + stickers, +}); + +export type AppState = typeof reducer extends Reducer ? U : never; diff --git a/sticker-creator/util/history.ts b/sticker-creator/util/history.ts new file mode 100644 index 000000000..b4b0ac01a --- /dev/null +++ b/sticker-creator/util/history.ts @@ -0,0 +1,3 @@ +import { createMemoryHistory } from 'history'; + +export const history = createMemoryHistory(); diff --git a/sticker-creator/util/i18n.tsx b/sticker-creator/util/i18n.tsx new file mode 100644 index 000000000..96b2d4a4a --- /dev/null +++ b/sticker-creator/util/i18n.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +export type I18nFn = ( + key: string, + substitutions?: Array +) => string; + +const I18nContext = React.createContext(() => 'NO LOCALE LOADED'); + +export type I18nProps = { + children: React.ReactNode; + messages: { [key: string]: { message: string } }; +}; + +export const I18n = ({ messages, children }: I18nProps) => { + const getMessage = React.useCallback( + (key, substitutions = []) => + substitutions.reduce( + (res, sub) => res.replace(/\$.+?\$/, sub), + messages[key].message + ), + [messages] + ); + + return ( + {children} + ); +}; + +export const useI18n = () => React.useContext(I18nContext); diff --git a/sticker-creator/util/preload.ts b/sticker-creator/util/preload.ts new file mode 100644 index 000000000..fbb4eba5e --- /dev/null +++ b/sticker-creator/util/preload.ts @@ -0,0 +1,27 @@ +export type WebpData = { + buffer: Buffer; + src: string; + path: string; +}; + +export type ConvertToWebpFn = ( + path: string, + width?: number, + height?: number +) => Promise; + +// @ts-ignore +export const convertToWebp: ConvertToWebpFn = window.convertToWebp; + +export type StickerData = { webp?: WebpData; emoji?: string }; +export type PackMetaData = { packId: string; key: string }; + +export type EncryptAndUploadFn = ( + manifest: { title: string; author: string }, + stickers: Array, + cover: WebpData, + onProgress?: () => unknown +) => Promise; + +// @ts-ignore +export const encryptAndUpload: EncryptAndUploadFn = window.encryptAndUpload; diff --git a/stylesheets/_fontfaces.scss b/stylesheets/_fontfaces.scss new file mode 100644 index 000000000..004e0b6b7 --- /dev/null +++ b/stylesheets/_fontfaces.scss @@ -0,0 +1,33 @@ +@font-face { + font-family: 'Inter'; + src: url('../fonts/inter-v3.10/Inter-BoldItalic.woff2'); + font-weight: bolder; + font-style: italic; +} +@font-face { + font-family: 'Inter'; + src: url('../fonts/inter-v3.10/Inter-Bold.woff2'); + font-weight: bolder; +} + +@font-face { + font-family: 'Inter'; + src: url('../fonts/inter-v3.10/Inter-SemiBoldItalic.woff2'); + font-weight: bold; + font-style: italic; +} +@font-face { + font-family: 'Inter'; + src: url('../fonts/inter-v3.10/Inter-Italic.woff2'); + font-style: italic; +} +@font-face { + font-family: 'Inter'; + src: url('../fonts/inter-v3.10/Inter-SemiBold.woff2'); + font-weight: bold; +} + +@font-face { + font-family: 'Inter'; + src: url('../fonts/inter-v3.10/Inter-Regular.woff2'); +} diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 725637b4e..370cd47c4 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -6179,6 +6179,7 @@ button.module-image__border-overlay:focus { padding: 0; justify-content: center; align-items: center; + @include font-body-1; @include light-theme { color: $color-gray-60; @@ -6288,6 +6289,7 @@ button.module-image__border-overlay:focus { @include emoji-size(16px); @include emoji-size(18px); @include emoji-size(20px); + @include emoji-size(24px); @include emoji-size(28px); @include emoji-size(32px); @include emoji-size(64px); diff --git a/stylesheets/_options.scss b/stylesheets/_options.scss index 6e8b6e1ae..6c1251893 100644 --- a/stylesheets/_options.scss +++ b/stylesheets/_options.scss @@ -1,3 +1,4 @@ +@import 'fontfaces'; @import 'variables'; @import '../node_modules/intl-tel-input/build/css/intlTelInput.css'; @import 'progress'; diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss index f2515bbc5..515816faf 100644 --- a/stylesheets/_variables.scss +++ b/stylesheets/_variables.scss @@ -1,37 +1,3 @@ -@font-face { - font-family: 'Inter'; - src: url('../fonts/inter-v3.10/Inter-BoldItalic.woff2'); - font-weight: bolder; - font-style: italic; -} -@font-face { - font-family: 'Inter'; - src: url('../fonts/inter-v3.10/Inter-Bold.woff2'); - font-weight: bolder; -} - -@font-face { - font-family: 'Inter'; - src: url('../fonts/inter-v3.10/Inter-SemiBoldItalic.woff2'); - font-weight: bold; - font-style: italic; -} -@font-face { - font-family: 'Inter'; - src: url('../fonts/inter-v3.10/Inter-Italic.woff2'); - font-style: italic; -} -@font-face { - font-family: 'Inter'; - src: url('../fonts/inter-v3.10/Inter-SemiBold.woff2'); - font-weight: bold; -} - -@font-face { - font-family: 'Inter'; - src: url('../fonts/inter-v3.10/Inter-Regular.woff2'); -} - $inter: Inter, 'Helvetica Neue', 'Source Sans Pro', 'Source Han Sans SC', 'Source Han Sans CN', 'Hiragino Sans GB', 'Hiragino Kaku Gothic', 'Microsoft Yahei UI', Helvetica, Arial, sans-serif; diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 3f7904de3..97e0704e1 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -1,4 +1,5 @@ // Global Settings, Variables, and Mixins +@import 'fontfaces'; @import 'variables'; @import 'mixins'; @import 'global'; diff --git a/stylesheets/manifest_bridge.scss b/stylesheets/manifest_bridge.scss new file mode 100644 index 000000000..e1fc3a7f7 --- /dev/null +++ b/stylesheets/manifest_bridge.scss @@ -0,0 +1,5 @@ +// This file acts as a bridge to allow BEM modules to work in the newer CSS +// modules development model +@import 'variables'; +@import 'mixins'; +@import 'modules'; diff --git a/test/app/fixtures/menu-mac-os-setup.json b/test/app/fixtures/menu-mac-os-setup.json index e26a7c7f8..aa619394f 100644 --- a/test/app/fixtures/menu-mac-os-setup.json +++ b/test/app/fixtures/menu-mac-os-setup.json @@ -48,6 +48,13 @@ { "label": "Set Up as New Device", "click": null + }, + { + "type": "separator" + }, + { + "label": "Create/upload sticker pack", + "click": null } ] }, diff --git a/test/app/fixtures/menu-mac-os.json b/test/app/fixtures/menu-mac-os.json index d4c81aa84..b4a705690 100644 --- a/test/app/fixtures/menu-mac-os.json +++ b/test/app/fixtures/menu-mac-os.json @@ -38,6 +38,15 @@ } ] }, + { + "label": "&File", + "submenu": [ + { + "label": "Create/upload sticker pack", + "click": null + } + ] + }, { "label": "&Edit", "submenu": [ diff --git a/test/app/fixtures/menu-windows-linux-setup.json b/test/app/fixtures/menu-windows-linux-setup.json index 2c1572300..258130862 100644 --- a/test/app/fixtures/menu-windows-linux-setup.json +++ b/test/app/fixtures/menu-windows-linux-setup.json @@ -13,6 +13,10 @@ { "type": "separator" }, + { + "label": "Create/upload sticker pack", + "click": null + }, { "accelerator": "CommandOrControl+,", "label": "Preferences…", diff --git a/test/app/fixtures/menu-windows-linux.json b/test/app/fixtures/menu-windows-linux.json index 905788e06..207c8494a 100644 --- a/test/app/fixtures/menu-windows-linux.json +++ b/test/app/fixtures/menu-windows-linux.json @@ -2,6 +2,10 @@ { "label": "&File", "submenu": [ + { + "label": "Create/upload sticker pack", + "click": null + }, { "accelerator": "CommandOrControl+,", "label": "Preferences…", diff --git a/test/app/menu_test.js b/test/app/menu_test.js index ff9537ebc..176605647 100644 --- a/test/app/menu_test.js +++ b/test/app/menu_test.js @@ -59,6 +59,7 @@ describe('SignalMenu', () => { showDebugLog: null, showKeyboardShortcuts: null, showSettings: null, + showStickerCreator: null, showWindow: null, }; const appLocale = 'en'; diff --git a/ts/components/emoji/Emoji.tsx b/ts/components/emoji/Emoji.tsx index d33907555..9203d0b92 100644 --- a/ts/components/emoji/Emoji.tsx +++ b/ts/components/emoji/Emoji.tsx @@ -6,7 +6,7 @@ export type OwnProps = { inline?: boolean; shortName: string; skinTone?: SkinToneKey | number; - size?: 16 | 18 | 20 | 28 | 32 | 64 | 66; + size?: 16 | 18 | 20 | 24 | 28 | 32 | 64 | 66; children?: React.ReactNode; }; diff --git a/ts/components/emoji/EmojiPicker.tsx b/ts/components/emoji/EmojiPicker.tsx index bcb048be0..8bbfef0ff 100644 --- a/ts/components/emoji/EmojiPicker.tsx +++ b/ts/components/emoji/EmojiPicker.tsx @@ -24,10 +24,10 @@ export type EmojiPickDataType = { skinTone?: number; shortName: string }; export type OwnProps = { readonly i18n: LocalizerType; readonly onPickEmoji: (o: EmojiPickDataType) => unknown; - readonly doSend: () => unknown; + readonly doSend?: () => unknown; readonly skinTone: number; readonly onSetSkinTone: (tone: number) => unknown; - readonly recentEmojis: Array; + readonly recentEmojis?: Array; readonly onClose: () => unknown; }; @@ -63,7 +63,7 @@ export const EmojiPicker = React.memo( onPickEmoji, skinTone = 0, onSetSkinTone, - recentEmojis, + recentEmojis = [], style, onClose, }: Props, @@ -126,7 +126,9 @@ export const EmojiPicker = React.memo( if ('key' in e) { if (e.key === 'Enter') { e.preventDefault(); - doSend(); + if (doSend) { + doSend(); + } } } else { const { shortName } = e.currentTarget.dataset; diff --git a/ts/components/emoji/lib.ts b/ts/components/emoji/lib.ts index edddb196c..f0e06d825 100644 --- a/ts/components/emoji/lib.ts +++ b/ts/components/emoji/lib.ts @@ -4,6 +4,7 @@ import emojiRegex from 'emoji-regex'; import { compact, flatMap, + get, groupBy, isNumber, keyBy, @@ -71,8 +72,16 @@ const data = (untypedData as Array).filter( emoji => emoji.has_img_apple ); +// @ts-ignore +const ROOT_PATH = get( + // tslint:disable-next-line no-typeof-undefined + typeof window !== 'undefined' ? window : null, + 'ROOT_PATH', + '' +); + const makeImagePath = (src: string) => { - return `node_modules/emoji-datasource-apple/img/apple/64/${src}`; + return `${ROOT_PATH}node_modules/emoji-datasource-apple/img/apple/64/${src}`; }; const imageQueue = new PQueue({ concurrency: 10 }); diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index eacefb316..dcdbe4759 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -1313,29 +1313,6 @@ "reasonCategory": "falseMatch", "updated": "2019-12-03T00:28:08.683Z" }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/@babel/runtime/node_modules/regenerator-runtime/runtime.js", - "line": " function wrap(innerFn, outerFn, self, tryLocsList) {", - "lineNumber": 36, - "reasonCategory": "falseMatch", - "updated": "2019-03-09T00:08:44.242Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/@babel/runtime/node_modules/regenerator-runtime/runtime.js", - "line": " wrap(innerFn, outerFn, self, tryLocsList)", - "lineNumber": 228, - "reasonCategory": "falseMatch", - "updated": "2019-03-09T00:08:44.242Z" - }, - { - "rule": "jQuery-$(", - "path": "node_modules/@develar/schema-utils/node_modules/ajv/dist/ajv.min.js", - "lineNumber": 2, - "reasonCategory": "falseMatch", - "updated": "2019-08-15T17:10:42.360Z" - }, { "rule": "eval", "path": "node_modules/@protobufjs/inquire/index.js", @@ -1408,537 +1385,6 @@ "reasonCategory": "falseMatch", "updated": "2019-07-31T00:19:18.696Z" }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/ansi-colors/index.js", - "line": " return typeof style === 'function' ? style(input) : style.wrap(input, newline);", - "lineNumber": 33, - "reasonCategory": "falseMatch", - "updated": "2019-08-19T18:03:38.741Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/ansi-colors/index.js", - "line": " while (n-- > 0) str = wrap(colors.styles[stack[n]], str, nl);", - "lineNumber": 46, - "reasonCategory": "falseMatch", - "updated": "2019-08-19T18:03:38.741Z" - }, - { - "rule": "jQuery-after(", - "path": "node_modules/archiver-utils/node_modules/lodash/after.js", - "line": " * var done = _.after(saves.length, function() {", - "lineNumber": 21, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-after(", - "path": "node_modules/archiver-utils/node_modules/lodash/after.js", - "line": "function after(n, func) {", - "lineNumber": 30, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/before.js", - "line": " * jQuery(element).on('click', _.before(5, addContactToList));", - "lineNumber": 20, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/before.js", - "line": "function before(n, func) {", - "lineNumber": 23, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/core.js", - "line": " * jQuery(element).on('click', _.before(5, addContactToList));", - "lineNumber": 2228, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/core.js", - "line": " function before(n, func) {", - "lineNumber": 2231, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/core.js", - "line": " return before(2, func);", - "lineNumber": 2381, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/core.js", - "line": " * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {", - "lineNumber": 3483, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-$(", - "path": "node_modules/archiver-utils/node_modules/lodash/core.min.js", - "lineNumber": 15, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/fp/_baseConvert.js", - "line": " function wrap(name, func, placeholder) {", - "lineNumber": 468, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/fp/_baseConvert.js", - "line": " return wrap(name, func, defaultHolder);", - "lineNumber": 521, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/fp/_baseConvert.js", - "line": " pairs.push([key, wrap(key, func, _)]);", - "lineNumber": 531, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/iteratee.js", - "line": " * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {", - "lineNumber": 40, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-after(", - "path": "node_modules/archiver-utils/node_modules/lodash/lodash.js", - "line": " * var done = _.after(saves.length, function() {", - "lineNumber": 9983, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-after(", - "path": "node_modules/archiver-utils/node_modules/lodash/lodash.js", - "line": " function after(n, func) {", - "lineNumber": 9992, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/lodash.js", - "line": " * jQuery(element).on('click', _.before(5, addContactToList));", - "lineNumber": 10041, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/lodash.js", - "line": " function before(n, func) {", - "lineNumber": 10044, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/lodash.js", - "line": " return before(2, func);", - "lineNumber": 10619, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/lodash.js", - "line": " * var p = _.wrap(_.escape, function(func, text) {", - "lineNumber": 10950, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/lodash.js", - "line": " function wrap(value, wrapper) {", - "lineNumber": 10957, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/lodash.js", - "line": " * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {", - "lineNumber": 15518, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver-utils/node_modules/lodash/once.js", - "line": " return before(2, func);", - "lineNumber": 22, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/wrap.js", - "line": " * var p = _.wrap(_.escape, function(func, text) {", - "lineNumber": 19, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver-utils/node_modules/lodash/wrap.js", - "line": "function wrap(value, wrapper) {", - "lineNumber": 26, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-append(", - "path": "node_modules/archiver/lib/core.js", - "line": " this._module.append(source, data, function(err) {", - "lineNumber": 179, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-append(", - "path": "node_modules/archiver/lib/plugins/tar.js", - "line": " function append(err, sourceBuffer) {", - "lineNumber": 68, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-append(", - "path": "node_modules/archiver/lib/plugins/tar.js", - "line": " append(null, source);", - "lineNumber": 80, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/async/dist/async.js", - "line": "function wrap(defer) {", - "lineNumber": 62, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/async/dist/async.js", - "line": "var setImmediate$1 = wrap(_defer);", - "lineNumber": 81, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-insertBefore(", - "path": "node_modules/archiver/node_modules/async/dist/async.js", - "line": " if (this.head) this.insertBefore(this.head, node);", - "lineNumber": 2151, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-insertAfter(", - "path": "node_modules/archiver/node_modules/async/dist/async.js", - "line": " if (this.tail) this.insertAfter(this.tail, node);", - "lineNumber": 2156, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/async/dist/async.js", - "line": "var nextTick = wrap(_defer$1);", - "lineNumber": 3823, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-insertBefore(", - "path": "node_modules/archiver/node_modules/async/dist/async.js", - "line": " q._tasks.insertBefore(nextNode, item);", - "lineNumber": 4107, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-$(", - "path": "node_modules/archiver/node_modules/async/dist/async.min.js", - "lineNumber": 1, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T21:59:32.770Z" - }, - { - "rule": "jQuery-insertAfter(", - "path": "node_modules/archiver/node_modules/async/dist/async.min.js", - "lineNumber": 1, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-insertBefore(", - "path": "node_modules/archiver/node_modules/async/dist/async.min.js", - "lineNumber": 1, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-insertBefore(", - "path": "node_modules/archiver/node_modules/async/internal/DoublyLinkedList.js", - "line": " if (this.head) this.insertBefore(this.head, node);else setInitial(this, node);", - "lineNumber": 52, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-insertAfter(", - "path": "node_modules/archiver/node_modules/async/internal/DoublyLinkedList.js", - "line": " if (this.tail) this.insertAfter(this.tail, node);else setInitial(this, node);", - "lineNumber": 56, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/async/internal/setImmediate.js", - "line": "function wrap(defer) {", - "lineNumber": 23, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/async/internal/setImmediate.js", - "line": "exports.default = wrap(_defer);", - "lineNumber": 42, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-insertBefore(", - "path": "node_modules/archiver/node_modules/async/priorityQueue.js", - "line": " q._tasks.insertBefore(nextNode, item);", - "lineNumber": 42, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-after(", - "path": "node_modules/archiver/node_modules/lodash/after.js", - "line": " * var done = _.after(saves.length, function() {", - "lineNumber": 21, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-after(", - "path": "node_modules/archiver/node_modules/lodash/after.js", - "line": "function after(n, func) {", - "lineNumber": 30, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/before.js", - "line": " * jQuery(element).on('click', _.before(5, addContactToList));", - "lineNumber": 20, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/before.js", - "line": "function before(n, func) {", - "lineNumber": 23, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/core.js", - "line": " * jQuery(element).on('click', _.before(5, addContactToList));", - "lineNumber": 2228, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/core.js", - "line": " function before(n, func) {", - "lineNumber": 2231, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/core.js", - "line": " return before(2, func);", - "lineNumber": 2381, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/core.js", - "line": " * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {", - "lineNumber": 3483, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-$(", - "path": "node_modules/archiver/node_modules/lodash/core.min.js", - "lineNumber": 15, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/fp/_baseConvert.js", - "line": " function wrap(name, func, placeholder) {", - "lineNumber": 468, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/fp/_baseConvert.js", - "line": " return wrap(name, func, defaultHolder);", - "lineNumber": 521, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/fp/_baseConvert.js", - "line": " pairs.push([key, wrap(key, func, _)]);", - "lineNumber": 531, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/iteratee.js", - "line": " * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {", - "lineNumber": 40, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-after(", - "path": "node_modules/archiver/node_modules/lodash/lodash.js", - "line": " * var done = _.after(saves.length, function() {", - "lineNumber": 9983, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-after(", - "path": "node_modules/archiver/node_modules/lodash/lodash.js", - "line": " function after(n, func) {", - "lineNumber": 9992, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/lodash.js", - "line": " * jQuery(element).on('click', _.before(5, addContactToList));", - "lineNumber": 10041, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/lodash.js", - "line": " function before(n, func) {", - "lineNumber": 10044, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/lodash.js", - "line": " return before(2, func);", - "lineNumber": 10619, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/lodash.js", - "line": " * var p = _.wrap(_.escape, function(func, text) {", - "lineNumber": 10950, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/lodash.js", - "line": " function wrap(value, wrapper) {", - "lineNumber": 10957, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/lodash.js", - "line": " * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {", - "lineNumber": 15518, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-before(", - "path": "node_modules/archiver/node_modules/lodash/once.js", - "line": " return before(2, func);", - "lineNumber": 22, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/wrap.js", - "line": " * var p = _.wrap(_.escape, function(func, text) {", - "lineNumber": 19, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/archiver/node_modules/lodash/wrap.js", - "line": "function wrap(value, wrapper) {", - "lineNumber": 26, - "reasonCategory": "falseMatch", - "updated": "2019-07-16T21:56:03.429Z" - }, { "rule": "jQuery-insertBefore(", "path": "node_modules/ast-types/lib/path.js", @@ -1955,6 +1401,256 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/build/index.js", + "line": " if (name.indexOf(ns + \".\") === 0 && !in$(name, experimental)) {", + "lineNumber": 36, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/build/index.js", + "line": " function in$(x, xs){", + "lineNumber": 99, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/core.js", + "line": " reCopy = new RegExp('^' + re.source + '$(?!\\\\s)', regexpFlags.call(re));", + "lineNumber": 2498, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/core.js", + "line": " return wrap(tag);", + "lineNumber": 4362, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/core.js", + "line": " return wrap(wks(name));", + "lineNumber": 4379, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/core.js", + "line": "var SUBSTITUTION_SYMBOLS = /\\$([$&`']|\\d\\d?|<[^>]*>)/g;", + "lineNumber": 6331, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/core.js", + "line": "var SUBSTITUTION_SYMBOLS_NO_NAMED = /\\$([$&`']|\\d\\d?)/g;", + "lineNumber": 6332, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/core.js", + "line": " setTimeout: wrap(global.setTimeout),", + "lineNumber": 8752, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/core.min.js", + "lineNumber": 7, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/core.min.js", + "lineNumber": 9, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/library.js", + "line": " return wrap(tag);", + "lineNumber": 4059, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/library.js", + "line": " return wrap(wks(name));", + "lineNumber": 4076, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/library.js", + "line": " setTimeout: wrap(global.setTimeout),", + "lineNumber": 7820, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/library.min.js", + "lineNumber": 7, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/library.min.js", + "lineNumber": 8, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/shim.js", + "line": " reCopy = new RegExp('^' + re.source + '$(?!\\\\s)', regexpFlags.call(re));", + "lineNumber": 2422, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/shim.js", + "line": " return wrap(tag);", + "lineNumber": 4260, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/shim.js", + "line": " return wrap(wks(name));", + "lineNumber": 4277, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/shim.js", + "line": "var SUBSTITUTION_SYMBOLS = /\\$([$&`']|\\d\\d?|<[^>]*>)/g;", + "lineNumber": 6229, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/shim.js", + "line": "var SUBSTITUTION_SYMBOLS_NO_NAMED = /\\$([$&`']|\\d\\d?)/g;", + "lineNumber": 6230, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/client/shim.js", + "line": " setTimeout: wrap(global.setTimeout),", + "lineNumber": 8650, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/shim.min.js", + "lineNumber": 7, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/client/shim.min.js", + "lineNumber": 8, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/library/modules/es6.symbol.js", + "line": " return wrap(tag);", + "lineNumber": 144, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/library/modules/es6.symbol.js", + "line": " return wrap(wks(name));", + "lineNumber": 161, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/library/modules/web.timers.js", + "line": " setTimeout: wrap(global.setTimeout),", + "lineNumber": 18, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/modules/_regexp-exec.js", + "line": " reCopy = new RegExp('^' + re.source + '$(?!\\\\s)', regexpFlags.call(re));", + "lineNumber": 34, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/modules/es6.regexp.replace.js", + "line": "var SUBSTITUTION_SYMBOLS = /\\$([$&`']|\\d\\d?|<[^>]*>)/g;", + "lineNumber": 12, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/attr-accept/node_modules/core-js/modules/es6.regexp.replace.js", + "line": "var SUBSTITUTION_SYMBOLS_NO_NAMED = /\\$([$&`']|\\d\\d?)/g;", + "lineNumber": 13, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/modules/es6.symbol.js", + "line": " return wrap(tag);", + "lineNumber": 144, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/modules/es6.symbol.js", + "line": " return wrap(wks(name));", + "lineNumber": 161, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-wrap(", + "path": "node_modules/attr-accept/node_modules/core-js/modules/web.timers.js", + "line": " setTimeout: wrap(global.setTimeout),", + "lineNumber": 18, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, { "rule": "jQuery-$(", "path": "node_modules/backbone/backbone-min.js", @@ -2861,139 +2557,12 @@ "updated": "2018-09-15T00:38:04.183Z" }, { - "rule": "jQuery-$(", - "path": "node_modules/core-js/build/index.js", - "line": " if (name.indexOf(ns + \".\") === 0 && !in$(name, experimental)) {", - "lineNumber": 43, + "rule": "jQuery-append(", + "path": "node_modules/copy-text-to-clipboard/index.js", + "line": "\tdocument.body.append(element);", + "lineNumber": 22, "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-$(", - "path": "node_modules/core-js/build/index.js", - "line": " function in$(x, xs){", - "lineNumber": 93, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/core.js", - "line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));", - "lineNumber": 1082, - "reasonCategory": "falseMatch" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/core.js", - "line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);", - "lineNumber": 1135, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/core.js", - "line": "\t setTimeout: wrap(global.setTimeout),", - "lineNumber": 4496, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/library.js", - "line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));", - "lineNumber": 1033, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/library.js", - "line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);", - "lineNumber": 1086, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/library.js", - "line": "\t setTimeout: wrap(global.setTimeout),", - "lineNumber": 4136, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/shim.js", - "line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));", - "lineNumber": 1068, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/shim.js", - "line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);", - "lineNumber": 1121, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/client/shim.js", - "line": "\t setTimeout: wrap(global.setTimeout),", - "lineNumber": 4482, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/library/modules/es6.symbol.js", - "line": " return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));", - "lineNumber": 142, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/library/modules/es6.symbol.js", - "line": " symbolStatics[it] = useNative ? sym : wrap(sym);", - "lineNumber": 195, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/library/modules/web.timers.js", - "line": " setTimeout: wrap(global.setTimeout),", - "lineNumber": 18, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:26:59.689Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/modules/es6.symbol.js", - "line": " return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));", - "lineNumber": 142, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/modules/es6.symbol.js", - "line": " symbolStatics[it] = useNative ? sym : wrap(sym);", - "lineNumber": 195, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:18:14.550Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/core-js/modules/web.timers.js", - "line": " setTimeout: wrap(global.setTimeout),", - "lineNumber": 18, - "reasonCategory": "falseMatch", - "updated": "2019-04-26T19:26:59.689Z" + "updated": "2019-12-11T01:10:06.091Z" }, { "rule": "jQuery-prepend(", @@ -3003,51 +2572,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, - { - "rule": "jQuery-$(", - "path": "node_modules/csso/dist/csso-browser.js", - "lineNumber": 2, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T21:59:32.770Z" - }, - { - "rule": "jQuery-prepend(", - "path": "node_modules/csso/dist/csso-browser.js", - "lineNumber": 3, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-prepend(", - "path": "node_modules/csso/node_modules/source-map/dist/source-map.debug.js", - "line": "\t this.prepend(aChunk[i]);", - "lineNumber": 2836, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-prepend(", - "path": "node_modules/csso/node_modules/source-map/dist/source-map.js", - "line": "\t this.prepend(aChunk[i]);", - "lineNumber": 2836, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-prepend(", - "path": "node_modules/csso/node_modules/source-map/dist/source-map.min.js", - "lineNumber": 1, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-prepend(", - "path": "node_modules/csso/node_modules/source-map/lib/source-node.js", - "line": " this.prepend(aChunk[i]);", - "lineNumber": 194, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, { "rule": "jQuery-load(", "path": "node_modules/debug/src/browser.js", @@ -3068,17 +2592,17 @@ "rule": "jQuery-load(", "path": "node_modules/debug/src/node.js", "line": "function load() {", - "lineNumber": 154, + "lineNumber": 156, "reasonCategory": "falseMatch", - "updated": "2018-09-15T00:38:04.183Z" + "updated": "2019-12-11T01:10:06.091Z" }, { "rule": "jQuery-load(", "path": "node_modules/debug/src/node.js", "line": "exports.enable(load());", - "lineNumber": 246, + "lineNumber": 248, "reasonCategory": "falseMatch", - "updated": "2018-09-15T00:38:04.183Z" + "updated": "2019-12-11T01:10:06.091Z" }, { "rule": "jQuery-wrap(", @@ -3369,6 +2893,38 @@ "reasonCategory": "falseMatch", "updated": "2019-04-10T19:08:25.356Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/electron-download/node_modules/debug/src/browser.js", + "line": "function load() {", + "lineNumber": 150, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/electron-download/node_modules/debug/src/browser.js", + "line": "exports.enable(load());", + "lineNumber": 168, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/electron-download/node_modules/debug/src/node.js", + "line": "function load() {", + "lineNumber": 154, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/electron-download/node_modules/debug/src/node.js", + "line": "exports.enable(load());", + "lineNumber": 246, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, { "rule": "jQuery-load(", "path": "node_modules/electron-notarize/node_modules/debug/dist/debug.js", @@ -3409,70 +2965,6 @@ "reasonCategory": "falseMatch", "updated": "2019-10-10T18:29:02.491Z" }, - { - "rule": "eval", - "path": "node_modules/electron/electron.d.ts", - "line": " eval(code: string): void;", - "lineNumber": 2136, - "reasonCategory": "falseMatch", - "updated": "2019-08-15T17:10:42.360Z" - }, - { - "rule": "jQuery-append(", - "path": "node_modules/electron/electron.d.ts", - "line": " append(menuItem: MenuItem): void;", - "lineNumber": 3760, - "reasonCategory": "falseMatch", - "updated": "2019-08-15T17:10:42.360Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/electron/node_modules/@types/node/globals.d.ts", - "line": " wrap(oldStream: ReadableStream): this;", - "lineNumber": 573, - "reasonCategory": "otherUtilityCode", - "updated": "2019-04-03T00:52:04.925Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/electron/node_modules/@types/node/globals.d.ts", - "line": " static wrap(code: string): string;", - "lineNumber": 976, - "reasonCategory": "otherUtilityCode", - "updated": "2019-04-03T00:52:04.925Z" - }, - { - "rule": "eval", - "path": "node_modules/electron/node_modules/@types/node/repl.d.ts", - "line": " * Default: an async wrapper for the JavaScript `eval()` function. An `eval` function can", - "lineNumber": 31, - "reasonCategory": "exampleCode", - "updated": "2019-04-03T00:52:04.925Z" - }, - { - "rule": "eval", - "path": "node_modules/electron/node_modules/@types/node/repl.d.ts", - "line": " * for the JavaScript `eval()` function.", - "lineNumber": 180, - "reasonCategory": "exampleCode", - "updated": "2019-04-03T00:52:04.925Z" - }, - { - "rule": "jQuery-wrap(", - "path": "node_modules/electron/node_modules/@types/node/stream.d.ts", - "line": " wrap(oldStream: NodeJS.ReadableStream): this;", - "lineNumber": 32, - "reasonCategory": "otherUtilityCode", - "updated": "2019-04-03T00:52:04.925Z" - }, - { - "rule": "jQuery-append(", - "path": "node_modules/electron/node_modules/@types/node/url.d.ts", - "line": " append(name: string, value: string): void;", - "lineNumber": 90, - "reasonCategory": "otherUtilityCode", - "updated": "2019-04-03T00:52:04.925Z" - }, { "rule": "thenify-multiArgs", "path": "node_modules/es6-promisify/dist/promisify.js", @@ -3513,82 +3005,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, - { - "rule": "jQuery-load(", - "path": "node_modules/extglob/node_modules/debug/src/browser.js", - "line": "function load() {", - "lineNumber": 150, - "reasonCategory": "falseMatch", - "updated": "2019-07-31T00:19:18.696Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/extglob/node_modules/debug/src/browser.js", - "line": "exports.enable(load());", - "lineNumber": 168, - "reasonCategory": "falseMatch", - "updated": "2019-07-31T00:19:18.696Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/extglob/node_modules/debug/src/node.js", - "line": "function load() {", - "lineNumber": 156, - "reasonCategory": "falseMatch", - "updated": "2019-07-31T00:19:18.696Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/extglob/node_modules/debug/src/node.js", - "line": "exports.enable(load());", - "lineNumber": 248, - "reasonCategory": "falseMatch", - "updated": "2019-07-31T00:19:18.696Z" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/fbjs/lib/createNodesFromMarkup.js", - "line": " node.innerHTML = wrap[1] + markup + wrap[2];", - "lineNumber": 58, - "reasonCategory": "ruleNeeded", - "updated": "2018-09-18T19:19:27.699Z", - "reasonDetail": "Need a rule for createNodesFromMarkup" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/fbjs/lib/createNodesFromMarkup.js", - "line": " node.innerHTML = markup;", - "lineNumber": 65, - "reasonCategory": "ruleNeeded", - "updated": "2018-09-18T19:19:27.699Z", - "reasonDetail": "Need a rule for createNodesFromMarkup" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/fbjs/lib/getMarkupWrap.js", - "line": " * Some browsers cannot use `innerHTML` to render certain elements standalone,", - "lineNumber": 23, - "reasonCategory": "falseMatch", - "updated": "2018-09-18T19:19:27.699Z" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/fbjs/lib/getMarkupWrap.js", - "line": " dummyNode.innerHTML = '';", - "lineNumber": 83, - "reasonCategory": "usageTrusted", - "updated": "2018-09-18T19:19:27.699Z", - "reasonDetail": "Hard-coded string" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/fbjs/lib/getMarkupWrap.js", - "line": " dummyNode.innerHTML = '<' + nodeName + '>';", - "lineNumber": 85, - "reasonCategory": "usageTrusted", - "updated": "2018-09-18T19:19:27.699Z", - "reasonDetail": "nodeName is limited to set of safe tag names." - }, { "rule": "jQuery-load(", "path": "node_modules/file-entry-cache/cache.js", @@ -3637,38 +3053,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, - { - "rule": "jQuery-load(", - "path": "node_modules/get-uri/node_modules/debug/src/browser.js", - "line": "function load() {", - "lineNumber": 150, - "reasonCategory": "falseMatch", - "updated": "2018-11-27T18:02:26.186Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/get-uri/node_modules/debug/src/browser.js", - "line": "exports.enable(load());", - "lineNumber": 168, - "reasonCategory": "falseMatch", - "updated": "2018-11-27T18:02:26.186Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/get-uri/node_modules/debug/src/node.js", - "line": "function load() {", - "lineNumber": 156, - "reasonCategory": "falseMatch", - "updated": "2018-11-27T18:02:26.186Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/get-uri/node_modules/debug/src/node.js", - "line": "exports.enable(load());", - "lineNumber": 248, - "reasonCategory": "falseMatch", - "updated": "2018-11-27T18:02:26.186Z" - }, { "rule": "thenify-multiArgs", "path": "node_modules/globby/node_modules/pify/index.js", @@ -3791,6 +3175,20 @@ "reasonCategory": "falseMatch", "updated": "2018-09-15T00:38:04.183Z" }, + { + "rule": "jQuery-$(", + "path": "node_modules/history/node_modules/js-tokens/index.js", + "lineNumber": 10, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-$(", + "path": "node_modules/history/umd/history.min.js", + "lineNumber": 1, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, { "rule": "jQuery-load(", "path": "node_modules/http-proxy-agent/node_modules/debug/src/browser.js", @@ -5183,6 +4581,14 @@ "reasonCategory": "falseMatch", "updated": "2019-08-15T17:10:42.360Z" }, + { + "rule": "jQuery-after(", + "path": "node_modules/mime/src/test.js", + "line": " after(function() {", + "lineNumber": 151, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, { "rule": "DOM-innerHTML", "path": "node_modules/min-document/serialize.js", @@ -5385,38 +4791,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, - { - "rule": "jQuery-load(", - "path": "node_modules/needle/node_modules/debug/src/browser.js", - "line": "function load() {", - "lineNumber": 150, - "reasonCategory": "falseMatch", - "updated": "2018-11-27T18:02:26.186Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/needle/node_modules/debug/src/browser.js", - "line": "exports.enable(load());", - "lineNumber": 168, - "reasonCategory": "falseMatch", - "updated": "2018-11-27T18:02:26.186Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/needle/node_modules/debug/src/node.js", - "line": "function load() {", - "lineNumber": 156, - "reasonCategory": "falseMatch", - "updated": "2018-11-27T18:02:26.186Z" - }, - { - "rule": "jQuery-load(", - "path": "node_modules/needle/node_modules/debug/src/node.js", - "line": "exports.enable(load());", - "lineNumber": 248, - "reasonCategory": "falseMatch", - "updated": "2018-11-27T18:02:26.186Z" - }, { "rule": "jQuery-insertBefore(", "path": "node_modules/neo-async/async.js", @@ -5469,27 +4843,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, - { - "rule": "jQuery-$(", - "path": "node_modules/node-forge/dist/forge.all.min.js", - "lineNumber": 4, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T21:59:32.770Z" - }, - { - "rule": "jQuery-$(", - "path": "node_modules/node-forge/dist/forge.min.js", - "lineNumber": 3, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T21:59:32.770Z" - }, - { - "rule": "jQuery-$(", - "path": "node_modules/node-forge/dist/prime.worker.min.js", - "lineNumber": 1, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T21:59:32.770Z" - }, { "rule": "jQuery-$(", "path": "node_modules/node-gyp/lib/configure.js", @@ -5610,6 +4963,38 @@ "reasonCategory": "falseMatch", "updated": "2019-07-19T17:16:02.404Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/nugget/node_modules/debug/src/browser.js", + "line": "function load() {", + "lineNumber": 150, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/nugget/node_modules/debug/src/browser.js", + "line": "exports.enable(load());", + "lineNumber": 168, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/nugget/node_modules/debug/src/node.js", + "line": "function load() {", + "lineNumber": 154, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/nugget/node_modules/debug/src/node.js", + "line": "exports.enable(load());", + "lineNumber": 246, + "reasonCategory": "falseMatch", + "updated": "2019-12-11T01:10:06.091Z" + }, { "rule": "jQuery-append(", "path": "node_modules/nugget/node_modules/form-data/lib/form_data.js", @@ -5650,24 +5035,6 @@ "reasonCategory": "falseMatch", "updated": "2019-07-19T17:16:02.404Z" }, - { - "rule": "jQuery-$(", - "path": "node_modules/object.getownpropertydescriptors/node_modules/es-abstract/operations/getOps.js", - "line": "var root = $(specHTML);", - "lineNumber": 23, - "reasonCategory": "falseMatch", - "updated": "2019-08-19T18:03:38.741Z", - "reasonDetail": "$ in this case is cheerio" - }, - { - "rule": "jQuery-$(", - "path": "node_modules/object.getownpropertydescriptors/node_modules/es-abstract/operations/getOps.js", - "line": " var op = $(x);", - "lineNumber": 30, - "reasonCategory": "falseMatch", - "updated": "2019-08-19T18:03:38.741Z", - "reasonDetail": "$ in this case is cheerio" - }, { "rule": "jQuery-wrap(", "path": "node_modules/optionator/lib/help.js", @@ -5804,14 +5171,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, - { - "rule": "thenify-multiArgs", - "path": "node_modules/path-type/node_modules/pify/index.js", - "line": "\t\t\t\t} else if (opts.multiArgs) {", - "lineNumber": 16, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:06:35.446Z" - }, { "rule": "thenify-multiArgs", "path": "node_modules/pify/index.js", @@ -5873,78 +5232,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, - { - "rule": "fbjs-createNodesFromMarkup", - "path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js", - "line": "function createNodesFromMarkup(markup, handleScript) {", - "lineNumber": 51, - "reasonCategory": "usageTrusted", - "updated": "2019-06-20T20:21:33.456Z" - }, - { - "rule": "fbjs-createNodesFromMarkup", - "path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js", - "line": " !!!dummyNode ? process.env.NODE_ENV !== 'production' ? invariant(false, 'createNodesFromMarkup dummy not initialized') : invariant(false) : void 0;", - "lineNumber": 53, - "reasonCategory": "falseMatch", - "updated": "2019-06-20T20:21:33.456Z" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js", - "line": " node.innerHTML = wrap[1] + markup + wrap[2];", - "lineNumber": 58, - "reasonCategory": "usageTrusted", - "updated": "2019-06-20T20:21:33.456Z" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js", - "line": " node.innerHTML = markup;", - "lineNumber": 65, - "reasonCategory": "usageTrusted", - "updated": "2019-06-20T20:21:33.456Z" - }, - { - "rule": "fbjs-createNodesFromMarkup", - "path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js", - "line": " !handleScript ? process.env.NODE_ENV !== 'production' ? invariant(false, 'createNodesFromMarkup(...): Unexpected