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}
+
+ >
+ );
+};
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]({cover.src})
+ ) : 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 }) => (
+
+));
+
+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}]({src})
+ ))}
+
+
+
+
+ );
+ }
+);
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]({image})
+
+
+ );
+};
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 + '>' + 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