From 980d64e19d7eb8ba670e78f1a515cd30946073bf Mon Sep 17 00:00:00 2001 From: alex baldwin Date: Tue, 28 Dec 2021 23:31:58 -0500 Subject: [PATCH 01/42] extract tag replacing logic --- src/server/tags.js | 45 +++++++++++++++++++ .../tests/{tumblr.test.js => tags.test.js} | 36 ++++++--------- 2 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 src/server/tags.js rename src/server/tests/{tumblr.test.js => tags.test.js} (65%) diff --git a/src/server/tags.js b/src/server/tags.js new file mode 100644 index 0000000..f478f0b --- /dev/null +++ b/src/server/tags.js @@ -0,0 +1,45 @@ +const _ = require('lodash'); + +class Tags { + constructor(options = {}) { + this.caseSensitive = options.caseSensitive || false; + this.allowDelete = options.allowDelete || false; + } + replace({ tags, find, replace }) { + /* eslint-disable prefer-const */ + let replaceableTags = _.concat([], replace); + let result = _.concat([], tags); + + if (replaceableTags.length === 0 && !this.allowDelete) { + throw new Error(`Can't replace tags with nothing unless deleting tags is allowed`); + } + + // loop through find to get matches + _.each(find, findTag => { + const matchIndex = _.findIndex(result, tag => { + if (this.caseSensitive) { + return tag === findTag; + } else { + return ( + (tag && tag.toLowerCase()) === + (findTag && findTag.toLowerCase()) + ); + } + }); + + if (matchIndex > -1) { + const replaceTag = replaceableTags.shift(); + result.splice(matchIndex, 1, replaceTag); + } + }); + + // if anything left over to replace, append to end + result = _.concat(result, replaceableTags); + + result = _.compact(result); + return result; + } + +} + +module.exports = Tags; \ No newline at end of file diff --git a/src/server/tests/tumblr.test.js b/src/server/tests/tags.test.js similarity index 65% rename from src/server/tests/tumblr.test.js rename to src/server/tests/tags.test.js index c46485d..a60ba34 100644 --- a/src/server/tests/tumblr.test.js +++ b/src/server/tests/tags.test.js @@ -2,15 +2,11 @@ const { expect } = require('chai'); -const TumblrClient = require('../tumblr'); +const Tags = require('../tags'); -const token = 'fake'; -const secret = 'also-fake'; - -describe('TumblrClient.replaceTags', function() { +describe('Tags.replace', function() { it('can replace 1 tag with 1 tag', function() { - const client = new TumblrClient({ token, secret }); - const result = client.replaceTags({ + const result = new Tags().replace({ tags: ['cat', 'dog'], find: ['cat'], replace: ['mouse'] @@ -20,8 +16,7 @@ describe('TumblrClient.replaceTags', function() { }); it('can replace 1 tag with 2 tags, preserving original order', function() { - const client = new TumblrClient({ token, secret }); - const result = client.replaceTags({ + const result = new Tags().replace({ tags: ['cat', 'dog'], find: ['cat'], replace: ['mouse', 'hamster'] @@ -31,8 +26,7 @@ describe('TumblrClient.replaceTags', function() { }); it('can append tags (replace 1 tag with itself and another tag)', function() { - const client = new TumblrClient({ token, secret }); - const result = client.replaceTags({ + const result = new Tags().replace({ tags: ['cat', 'dog', 'bird'], find: ['dog'], replace: ['dog', 'hamster'] @@ -42,10 +36,9 @@ describe('TumblrClient.replaceTags', function() { }); it('can remove a tag (replace 1 tag with 0 tags)', function() { - const client = new TumblrClient({ token, secret, options: { + const result = new Tags({ allowDelete: true, - }}); - const result = client.replaceTags({ + }).replace({ tags: ['cat', 'dog', 'bird'], find: ['dog'], replace: [], @@ -55,10 +48,9 @@ describe('TumblrClient.replaceTags', function() { }); it('can remove multiple tags (replace 2+ tags with 0 tags)', function() { - const client = new TumblrClient({ token, secret, options: { + const result = new Tags({ allowDelete: true, - }}); - const result = client.replaceTags({ + }).replace({ tags: ['cat', 'dog', 'bird'], find: ['cat', 'dog'], replace: [], @@ -68,10 +60,9 @@ describe('TumblrClient.replaceTags', function() { }); it('can find tags case-insensitively', function() { - const client = new TumblrClient({ token, secret, options: { + const result = new Tags({ caseSensitive: false, - }}); - const result = client.replaceTags({ + }).replace({ tags: ['cat', 'Dog'], find: ['Cat'], replace: ['Mouse'], @@ -81,10 +72,9 @@ describe('TumblrClient.replaceTags', function() { }); it('can find tags case-sensitively', function() { - const client = new TumblrClient({ token, secret, options: { + const result = new Tags({ caseSensitive: true, - }}); - const result = client.replaceTags({ + }).replace({ tags: ['cat', 'Cat', 'Dog'], find: ['Cat'], replace: ['Mouse'], From 7beccf05a705b5c8a2f3df3290b17795106d3add Mon Sep 17 00:00:00 2001 From: alex baldwin Date: Wed, 29 Dec 2021 20:14:28 -0500 Subject: [PATCH 02/42] use Tags in tumblr client --- src/server/tumblr.js | 42 ++++-------------------------------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/src/server/tumblr.js b/src/server/tumblr.js index 38e3aca..e624a4c 100644 --- a/src/server/tumblr.js +++ b/src/server/tumblr.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const tumblr = require('tumblr.js'); const Sentry = require('@sentry/node'); +const Tags = require('./tags'); const logger = require('./logger'); const POST_LIMIT = 20; @@ -52,6 +53,9 @@ class TumblrClient { }, returnPromises: true })); + + const tags = new Tags(options); + this.replaceTags = tags.replace; this.blog = blog; this.options = _.assign({}, DEFAULT_OPTIONS, options); @@ -203,44 +207,6 @@ class TumblrClient { }); } - /** - * find and replace in array of tags - * @param {string[]} tags post tags - * @param {string[]} find tags to find - * @param {string[]} replace replacement tags - * @return {string[]} replaced tags - */ - replaceTags({ tags, find, replace }) { - /* eslint-disable prefer-const */ - let replaceableTags = _.concat([], replace); - let result = _.concat([], tags); - - // loop through find to get matches - _.each(find, findTag => { - const matchIndex = _.findIndex(result, tag => { - if (this.options.caseSensitive) { - return tag === findTag; - } else { - return ( - (tag && tag.toLowerCase()) === - (findTag && findTag.toLowerCase()) - ); - } - }); - - if (matchIndex > -1) { - const replaceTag = replaceableTags.shift(); - result.splice(matchIndex, 1, replaceTag); - } - }); - - // if anything left over to replace, append to end - result = _.concat(result, replaceableTags); - - result = _.compact(result); - return result; - } - /** * [findPostsWithTags description] * @param {[type]} input [description] From 46d4676b5db0f51de7ae82fbbf70f81dcde6eb6d Mon Sep 17 00:00:00 2001 From: alex baldwin Date: Wed, 29 Dec 2021 22:31:46 -0500 Subject: [PATCH 03/42] refactor tumblr client to use event emitters --- src/server/tumblr.js | 325 ++++++++++++++++++++----------------------- 1 file changed, 153 insertions(+), 172 deletions(-) diff --git a/src/server/tumblr.js b/src/server/tumblr.js index e624a4c..6605338 100644 --- a/src/server/tumblr.js +++ b/src/server/tumblr.js @@ -1,5 +1,6 @@ require('dotenv').config(); +const EventEmitter = require('events'); const _ = require('lodash'); const tumblr = require('tumblr.js'); const Sentry = require('@sentry/node'); @@ -8,6 +9,7 @@ const Tags = require('./tags'); const logger = require('./logger'); const POST_LIMIT = 20; +const REPLACE_SOFT_LIMIT = 500 - POST_LIMIT; /** * @typedef {Object} Options @@ -23,18 +25,48 @@ const DEFAULT_OPTIONS = { allowDelete: false, }; -const EMPTY_RESPONSE = { - posts: [], - queued: [], - drafts: [], -}; +const sleep = ms => new Promise(resolve => { + setTimeout(resolve, ms); +}); +/** + * @typedef MethodName + * @property {string} POSTS posts + * @property {string} QUEUED queued + * @property {string} DRAFTS drafts + */ const METHODS = { - posts: 'posts', - queued: 'queued', - drafts: 'drafts', + POSTS: 'posts', + QUEUED: 'queued', + DRAFTS: 'drafts', }; +/** + * @typedef {Object} Method + * @property {MethodName} key + * @property {string} clientMethod tumblr.js client method name + * @property {string} nextParam pagination key found in _links.next.query_params + */ +const METHODS_CONFIG = { + // https://www.tumblr.com/docs/en/api/v2#posts + [METHODS.POSTS]: { + key: METHODS.POSTS, + clientMethod: 'blogPosts', + nextParam: 'page_number', + }, + // https://www.tumblr.com/docs/en/api/v2#blog-queue + [METHODS.QUEUED]: { + key: METHODS.QUEUED, + clientMethod: 'blogQueue', + nextParam: 'offset', + }, + // https://www.tumblr.com/docs/en/api/v2#blog-drafts + [METHODS.DRAFTS]: { + key: METHODS.DRAFTS, + clientMethod: 'blogDrafts', + nextParam: 'before_id', + } +}; class TumblrClient { /** @@ -59,9 +91,10 @@ class TumblrClient { this.blog = blog; this.options = _.assign({}, DEFAULT_OPTIONS, options); - this._results = {}; } + static methods = METHODS_CONFIG; + /** * wrap default logger with blog and options context */ @@ -91,49 +124,7 @@ class TumblrClient { return new Proxy(client, proxyHandler); } - /** - * @typedef {Object} Method - * @property {string} key key to use in results object - * @property {string} clientMethod tumblr.js client method name - * @property {string} nextParam pagination key found in _links.next.query_params - */ - - /** - * array of enabled API methods for fetching posts - * @return {Method[]} array of method definitions - */ - get methods() { - var methods = [ - { - // https://www.tumblr.com/docs/en/api/v2#posts - key: METHODS.posts, - clientMethod: 'blogPosts', - nextParam: 'page_number', - } - ]; - - if (this.options.includeQueue) { - methods.push({ - // https://www.tumblr.com/docs/en/api/v2#blog-queue - key: METHODS.queued, - clientMethod: 'blogQueue', - nextParam: 'offset', - }); - } - - if (this.options.includeDrafts) { - // https://www.tumblr.com/docs/en/api/v2#blog-drafts - methods.push({ - key: METHODS.drafts, - clientMethod: 'blogDrafts', - nextParam: 'before_id', - }); - } - - return methods; - } - - /** + /** * get authenticated user's info * https://www.tumblr.com/docs/en/api/v2#user-methods * @return {Promise} API response @@ -144,131 +135,125 @@ class TumblrClient { /** * find all posts with a tag and method - * @param {string} tag tag to find + * @param {String} tag tag to find * @param {Method} method method to use - * @param {Object} [params={}] additional parameters - * @param {boolean} [retry=false] whether this attempt is a retry - * @return {Promise} promise resolving an array of posts + * @return {EventEmitter} */ - findPosts({ tag, method, params = {}, retry = false }) { - if (!_.get(this._results, method.key)) { - /* - TODO: pagination client should be separate from 'tag replacing' client so - this can be naive. or just use a while loop instead of recursion. lmao - */ - this._results[method.key] = []; - } - - return this.client[method.clientMethod](this.blog, _.assign({ - tag: tag, - limit: POST_LIMIT, - filter: 'text', - }, params)).then(response => { - - let posts; - if (this.options.caseSensitive) { - posts = _.filter(response.posts, post => _.includes(post.tags, tag)); - } else if (method.key !== METHODS.posts) { - // draft and queue methods don't support the tag param 🙄 - posts = _.filter(response.posts, post => ( - _.includes(post.tags.map(t => t.toLowerCase()), tag.toLowerCase()) - )); - } else { - posts = response.posts; - } + findPosts({ tag, method }) { + const results = []; + const emitter = new EventEmitter(); + + (async () => { + let next = undefined; + let retry = false; + + while (next !== false) { + try { + const response = await this.client[method.clientMethod](this.blog, { + tag: tag, + limit: POST_LIMIT, + filter: 'text', + [method.nextParam]: next, + }); - this._results[method.key].push(...posts); + let posts; + if (this.options.caseSensitive) { + posts = _.filter(response.posts, post => _.includes(post.tags, tag)); + } else if (method.key !== METHODS.posts) { + // draft and queue methods don't support the tag param 🙄 + posts = _.filter(response.posts, post => ( + _.includes(post.tags.map(t => t.toLowerCase()), tag.toLowerCase()) + )); + } else { + posts = response.posts; + } - if (_.get(response, '_links.next')) { - const next = response._links.next; - const nextParams = { - [method.nextParam]: next.query_params[method.nextParam], - }; + results.push(...posts); + emitter.emit('data', posts); - return this.findPosts({ - tag, - method, - params: nextParams, - }); - } else { - return this._results[method.key]; - } - }).catch(error => { - // retry once - if (!retry) { - return this.findPosts({ - tag, - method, - retry: true - }); - } else { - throw error; + if (_.get(response, '_links.next')) { + next = response._links.next.query_params[method.nextParam]; + await sleep(500); + } else { + next = false; + emitter.emit('end', results); + } + } catch (error) { + // retry once + if (!retry) { + retry = true; + await sleep(500); + } else { + emitter.emit('error', error); + break; + } + } } - }); + })(); + + return emitter; } /** * [findPostsWithTags description] - * @param {[type]} input [description] - * @return {[type]} [description] + * @param {MethodName} methodName + * @param {String[]} find + * @return {EventEmitter} */ - findPostsWithTags(find) { - if (!_.isArray(find)) return Promise.reject(`expected 'find' to be an Array, but it was ${typeof find}`); + findPostsWithTags(methodName, find) { + if (!_.isArray(find)) throw new Error(`expected 'find' to be an Array, but it was ${typeof find}`); + + const emitter = new EventEmitter(); const tags = _.chain(find) .sortBy() .value(); const firstTag = tags[0]; - - var promises = this.methods.map(method => { - return this.findPosts({ tag: firstTag, method }) - .then(posts => { - // if multiple tags, filter on results from first tag - if (tags.length > 1) { - return _.filter(posts, post => { - const sortedPostTags = _.sortBy(post.tags); - return _.isEqual( - _.intersection(sortedPostTags, tags), - tags - ); - }); - } else { - return posts; - } - }) - .then(posts => ({ - [method.key]: posts - })); - }); - - return Promise.all(promises) - .then(results => results.reduce((a, v) => _.assign(a, v), {})) - .then(results => _.assign({}, EMPTY_RESPONSE, results)) - .then(results => { - this.log('find', { - find, - results: { - length: Object.keys(results).reduce((a, v) => a + results[v].length, 0) - } - }); - return results; - }); + const method = METHODS_CONFIG[methodName]; + + this.findPosts({ tag: firstTag, method }) + .on('data', (posts) => { + // if multiple tags, filter on results from first tag + if (tags.length > 1) { + const postsWithAllTags = _.filter(posts, post => { + const sortedPostTags = _.sortBy(post.tags); + return _.isEqual( + _.intersection(sortedPostTags, tags), + tags + ); + }); + emitter.emit('data', postsWithAllTags); + } else { + emitter.emit('data', posts); + } + }) + .on('error', error => ( + emitter.emit('error', error) + )) + .on('end', posts => ( + emitter.emit('end', posts) + )); + + return emitter; } /** * [findAndReplaceTags description] - * @param {[type]} find [description] - * @param {[type]} replace [description] - * @return {[type]} [description] + * @param {MethodName} methodName + * @param {String[]} find + * @param {String[]} replace + * @return {EventEmitter} */ - findAndReplaceTags(find, replace) { - if (!_.isArray(find)) return Promise.reject(`expected 'find' to be an Array, but it was ${typeof find}`); - if (!_.isArray(replace)) return Promise.reject(`expected 'replace' to be an Array, but it was ${typeof find}`); - - return this.findPostsWithTags(find) - .then(results => { - const promises = _.chain(this.methods) - .flatMap(method => results[method.key]) + findAndReplaceTags(methodName, find, replace) { + if (!_.isArray(find)) throw new Error(`expected 'find' to be an Array, but it was ${typeof find}`); + if (!_.isArray(replace)) throw new Error(`expected 'replace' to be an Array, but it was ${typeof find}`); + + const emitter = new EventEmitter(); + + this.findPostsWithTags(methodName, find) + .on('end', (posts) => { + const promises = posts + .slice(0, REPLACE_SOFT_LIMIT) .map(post => { const replacedTags = this.replaceTags({ tags: post.tags, @@ -278,25 +263,21 @@ class TumblrClient { return this.client.editPost(this.blog, { id: post.id, - tags: replacedTags.join(',') + tags: replacedTags.join(','), + }).then((response) => { + emitter.emit('data', response); + return response; }); - }) - .value(); - - return Promise.all(promises) - .then(replaced => { - this.log('replace', { - find, - replace, - replaced: { - length: replaced.length - } }); - return results; - }); + + Promise.all(promises) + .then(replaced => ( + emitter.emit('end', replaced) + )); }); + + return emitter; } } - -module.exports = TumblrClient; +module.exports = TumblrClient; \ No newline at end of file From cf63ba5006b54ab59f05bdbc277e797e721ad16d Mon Sep 17 00:00:00 2001 From: alex baldwin Date: Fri, 31 Dec 2021 02:29:27 -0500 Subject: [PATCH 04/42] websockets, bullmq setup --- eslint.js | 136 +------------------------ package.json | 9 +- src/client/app.js | 9 ++ src/client/state/store.js | 1 + src/index.js | 43 +++++--- src/server/queues.js | 10 ++ src/server/websockets.js | 26 +++++ src/server/workers/find.js | 3 + src/server/workers/index.js | 23 +++++ src/server/workers/replace.js | 3 + webpack.config.js | 3 + yarn.lock | 181 +++++++++++++++++++++++++++++++++- 12 files changed, 296 insertions(+), 151 deletions(-) create mode 100644 src/server/queues.js create mode 100644 src/server/websockets.js create mode 100644 src/server/workers/find.js create mode 100644 src/server/workers/index.js create mode 100644 src/server/workers/replace.js diff --git a/eslint.js b/eslint.js index f3950bf..8497115 100644 --- a/eslint.js +++ b/eslint.js @@ -2,6 +2,11 @@ module.exports = { root: true, parser: 'babel-eslint', plugins: ['jsx-a11y', 'react'], + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:jsx-a11y/recommended', + ], env: { browser: true, @@ -29,135 +34,4 @@ module.exports = { }, }, }, - - rules: { - // http://eslint.org/docs/rules/ - 'array-callback-return': 'warn', - 'default-case': ['warn', { commentPattern: '^no default$' }], - 'dot-location': ['warn', 'property'], - eqeqeq: ['warn', 'allow-null'], - 'guard-for-in': 'warn', - indent: 'off', - 'new-cap': ['warn', { newIsCap: true }], - 'new-parens': 'warn', - 'no-array-constructor': 'warn', - 'no-caller': 'warn', - 'no-cond-assign': ['warn', 'always'], - 'no-const-assign': 'warn', - 'no-control-regex': 'warn', - 'no-delete-var': 'warn', - 'no-dupe-args': 'warn', - 'no-dupe-class-members': 'warn', - 'no-dupe-keys': 'warn', - 'no-duplicate-case': 'warn', - 'no-empty-character-class': 'warn', - 'no-empty-pattern': 'warn', - 'no-eval': 'warn', - 'no-ex-assign': 'warn', - 'no-extend-native': 'warn', - 'no-extra-bind': 'warn', - 'no-extra-label': 'warn', - 'no-fallthrough': 'warn', - 'no-func-assign': 'warn', - 'no-implied-eval': 'warn', - 'no-invalid-regexp': 'warn', - 'no-iterator': 'warn', - 'no-label-var': 'warn', - 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }], - 'no-lone-blocks': 'warn', - 'no-loop-func': 'warn', - 'no-mixed-spaces-and-tabs': 'warn', - 'no-mixed-operators': [ - 'warn', - { - groups: [ - ['&', '|', '^', '~', '<<', '>>', '>>>'], - ['==', '!=', '===', '!==', '>', '>=', '<', '<='], - ['&&', '||'], - ['in', 'instanceof'], - ], - allowSamePrecedence: false, - }, - ], - 'no-multi-str': 'warn', - 'no-native-reassign': 'warn', - 'no-negated-in-lhs': 'warn', - 'no-new-func': 'warn', - 'no-new-object': 'warn', - 'no-new-symbol': 'warn', - 'no-new-wrappers': 'warn', - 'no-obj-calls': 'warn', - 'no-octal': 'warn', - 'no-octal-escape': 'warn', - 'no-redeclare': 'warn', - 'no-regex-spaces': 'warn', - 'no-restricted-syntax': ['warn', 'LabeledStatement', 'WithStatement'], - 'no-return-assign': 'warn', - 'no-script-url': 'warn', - 'no-self-assign': 'warn', - 'no-self-compare': 'warn', - 'no-sequences': 'warn', - 'no-shadow-restricted-names': 'warn', - 'no-sparse-arrays': 'warn', - 'no-this-before-super': 'warn', - 'no-throw-literal': 'warn', - 'no-undef': 'warn', - 'no-unexpected-multiline': 'warn', - 'no-unneeded-ternary': 'warn', - 'no-unreachable': 'warn', - 'no-unused-expressions': 'warn', - 'no-unused-labels': 'warn', - 'no-unused-vars': ['warn', { vars: 'local', args: 'none' }], - 'no-use-before-define': ['warn', 'nofunc'], - 'no-useless-computed-key': 'warn', - 'no-useless-concat': 'warn', - 'no-useless-constructor': 'warn', - 'no-useless-escape': 'off', - 'no-useless-rename': [ - 'warn', - { - ignoreDestructuring: false, - ignoreImport: false, - ignoreExport: false, - }, - ], - 'no-with': 'warn', - 'no-whitespace-before-property': 'warn', - 'operator-assignment': ['warn', 'always'], - 'operator-linebreak': 'off', - 'prefer-const': 'warn', - radix: 'warn', - 'require-yield': 'warn', - 'rest-spread-spacing': ['warn', 'never'], - semi: ['warn', 'always'], - strict: ['warn', 'never'], - 'unicode-bom': ['warn', 'never'], - 'use-isnan': 'warn', - 'valid-typeof': 'warn', - - // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules - 'react/jsx-equals-spacing': ['warn', 'never'], - 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }], - 'react/jsx-no-undef': 'warn', - 'react/jsx-pascal-case': [ - 'warn', - { - allowAllCaps: true, - ignore: [], - }, - ], - 'react/jsx-uses-react': 'warn', - 'react/jsx-uses-vars': 'warn', - 'react/no-deprecated': 'warn', - 'react/no-direct-mutation-state': 'warn', - 'react/no-is-mounted': 'warn', - 'react/react-in-jsx-scope': 'warn', - 'react/require-render-return': 'warn', - - // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules - 'jsx-a11y/aria-role': 'warn', - // 'jsx-a11y/img-has-alt': 'warn', - 'jsx-a11y/img-redundant-alt': 'warn', - 'jsx-a11y/no-access-key': 'warn', - }, }; diff --git a/package.json b/package.json index 02386f1..4786008 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@sentry/node": "^6.11.0", "@sentry/tracing": "^6.11.0", "body-parser": "^1.19.0", + "bullmq": "^1.60.0", "connect-redis": "^3.4.2", "debug": "^3.2.6", "envify": "^4.1.0", @@ -40,9 +41,12 @@ "react-markdown": "^2.5.0", "redis": "^2.8.0", "sass": "^1.38.0", + "serialize-javascript": "^6.0.0", "stringify": "^5.1.0", "tumblr.js": "https://github.com/cubeghost/tumblr.js", - "winston": "^3.2.1" + "uuid": "^8.3.2", + "winston": "^3.2.1", + "ws": "^8.4.0" }, "devDependencies": { "@babel/core": "^7.7.4", @@ -97,5 +101,8 @@ }, "eslintConfig": { "extends": "./eslint.js" + }, + "optionalDependencies": { + "bufferutil": "^4.0.5" } } diff --git a/src/client/app.js b/src/client/app.js index 48895b9..1bcb7f4 100644 --- a/src/client/app.js +++ b/src/client/app.js @@ -23,6 +23,15 @@ const mapDispatchToProps = dispatch => ({ getUser: () => dispatch(getUser()), }); +const socket = new WebSocket(process.env.WEBSOCKET_HOST); +socket.onopen = (event) => { + console.log('event', event) + console.log('socket', socket) +}; +socket.addEventListener('message', (event) => { + console.log('ws', event.data); +}); + class App extends Component { componentDidMount() { diff --git a/src/client/state/store.js b/src/client/state/store.js index 3522409..9fc3fcd 100644 --- a/src/client/state/store.js +++ b/src/client/state/store.js @@ -1,5 +1,6 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; + import reducers from './reducers'; const ENABLE_REDUX_DEVTOOLS = diff --git a/src/index.js b/src/index.js index 855e408..9d24bb4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,24 @@ require('dotenv').config(); +const http = require('http'); const express = require('express'); -const session = require('express-session'); -const RedisStore = require('connect-redis')(session); +const Session = require('express-session'); +const RedisStore = require('connect-redis')(Session); const Grant = require('grant-express'); const Sentry = require('@sentry/node'); const Tracing = require('@sentry/tracing'); const helmet = require('helmet'); +const WebSocket = require('ws'); const client = require('./server/redis'); const { webRouter, apiRouter } = require('./server/router'); +const webSocketHandler = require('./server/websockets'); const logger = require('./server/logger'); // setup const app = express(); +const server = http.createServer(app); +const wss = new WebSocket.Server({ server }); if (process.env.SENTRY_DSN) { Sentry.init({ @@ -42,6 +47,7 @@ const grant = new Grant({ tumblr: { authorize_url: 'https://www.tumblr.com/oauth2/authorize', access_url: 'https://api.tumblr.com/v2/oauth2/token', + origin: `${process.env.PROTOCOL}://${process.env.HOST_HOSTNAME}`, oauth: 2, scope: ['write'], key: process.env.TUMBLR_API_KEY, @@ -49,19 +55,18 @@ const grant = new Grant({ }, }); -app.use( - session({ - store: new RedisStore({ - client: client, - disableTouch: true, - }), - name: 'tagreplacer_session', - resave: false, - secure: process.env.PROTOCOL === 'https', - saveUninitialized: false, - secret: process.env.SECRET, - }) -); +const session = Session({ + store: new RedisStore({ + client: client, + disableTouch: true, + }), + name: 'tagreplacer_session', + resave: false, + secure: process.env.PROTOCOL === 'https', + saveUninitialized: false, + secret: process.env.SECRET, +}); +app.use(session); app.use(grant); @@ -91,6 +96,12 @@ app.get('/disconnect', (req, res) => { req.session.destroy(err => res.redirect('/')); }); +wss.on('connection', (ws, req) => ( + session(req, {}, () => ( + webSocketHandler(ws, req) + )) +)); + if (process.env.SENTRY_DSN) { app.use(Sentry.Handlers.errorHandler()); } @@ -106,6 +117,6 @@ app.use((error, req, res, next) => { }); // listen -app.listen(process.env.PORT, () => { +server.listen(process.env.PORT, () => { logger.info('express server started', { port: process.env.PORT }); }); diff --git a/src/server/queues.js b/src/server/queues.js new file mode 100644 index 0000000..8352681 --- /dev/null +++ b/src/server/queues.js @@ -0,0 +1,10 @@ +const FIND_QUEUE = 'tumblr:find'; +const REPLACE_QUEUE = 'tumblr:replace'; + +const MESSAGE_QUEUE = sessionId => `messages:${sessionId}`; + +module.exports = { + FIND_QUEUE, + REPLACE_QUEUE, + MESSAGE_QUEUE, +}; \ No newline at end of file diff --git a/src/server/websockets.js b/src/server/websockets.js new file mode 100644 index 0000000..db6f787 --- /dev/null +++ b/src/server/websockets.js @@ -0,0 +1,26 @@ +const { Worker } = require('bullmq'); +const serialize = require('serialize-javascript'); + +const { MESSAGE_QUEUE } = require('./queues'); + +module.exports = (ws, req) => { + const sessionId = req.session.id; + const hasTumblrSession = Boolean(req.session.grant && req.session.grant.response && !req.session.grant.response.error); + + if (!hasTumblrSession) { + ws.close(); + return; + } + + const worker = new Worker(MESSAGE_QUEUE(sessionId), async (job) => { + ws.send(serialize(job.data)); + }); + + ws.on('message', (message) => { + console.log(`Received message ${message} from user ${sessionId}`); + }); + + ws.on('close', async () => { + await worker.close(); + }); +}; diff --git a/src/server/workers/find.js b/src/server/workers/find.js new file mode 100644 index 0000000..2220043 --- /dev/null +++ b/src/server/workers/find.js @@ -0,0 +1,3 @@ +module.exports = async (job) => { + +}; \ No newline at end of file diff --git a/src/server/workers/index.js b/src/server/workers/index.js new file mode 100644 index 0000000..afdc6fe --- /dev/null +++ b/src/server/workers/index.js @@ -0,0 +1,23 @@ +const path = require('path'); +const { Worker } = require('bullmq'); + +const { FIND_QUEUE, REPLACE_QUEUE } = require('../queues'); + +const findProcessor = path.join(__dirname, 'find.js'); +const findWorker = new Worker(FIND_QUEUE, findProcessor); + +const replaceProcessor = path.join(__dirname, 'replace.js'); +const replaceWorker = new Worker(REPLACE_QUEUE, replaceProcessor); + +findWorker.on('error', err => { + console.error(err); +}); + +replaceWorker.on('error', err => { + console.error(err); +}); + +module.exports = { + findWorker, + replaceWorker, +}; \ No newline at end of file diff --git a/src/server/workers/replace.js b/src/server/workers/replace.js new file mode 100644 index 0000000..2220043 --- /dev/null +++ b/src/server/workers/replace.js @@ -0,0 +1,3 @@ +module.exports = async (job) => { + +}; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 37d8ef1..8d563b6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -144,6 +144,9 @@ const config = { }, }), new webpack.EnvironmentPlugin(['NODE_ENV', 'TESTING_BLOG', 'SENTRY_DSN']), + new webpack.DefinePlugin({ + 'process.env.WEBSOCKET_HOST': `'${process.env.PROTOCOL.replace('http', 'ws')}://${process.env.HOST_HOSTNAME}'`, + }), new CaseSensitivePathsPlugin(), new FriendlyErrorsWebpackPlugin(), new CleanWebpackPlugin([paths.appBuild]) diff --git a/yarn.lock b/yarn.lock index 4346945..391f17c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,11 +1972,33 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" +bufferutil@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.5.tgz#da9ea8166911cc276bf677b8aed2d02d31f59028" + integrity sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A== + dependencies: + node-gyp-build "^4.3.0" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bullmq@^1.60.0: + version "1.60.0" + resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.60.0.tgz#5d822f9e3b92767e0cc6a33ace5b2709b6e83a83" + integrity sha512-5TufCrbj48nTrLuWXByGkH7xa24HwvwiIJ46RxbInJZPgiHe6sRl0fBkVp1XpUNzosBrN7QosF9Y4tD82ZWxrQ== + dependencies: + cron-parser "^2.18.0" + get-port "^5.1.1" + glob "^7.2.0" + ioredis "^4.28.2" + lodash "^4.17.21" + msgpackr "^1.4.6" + semver "^6.3.0" + tslib "^1.14.1" + uuid "^8.3.2" + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -2329,6 +2351,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -2657,6 +2684,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cron-parser@^2.18.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf" + integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg== + dependencies: + is-nan "^1.3.0" + moment-timezone "^0.5.31" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -2809,6 +2844,13 @@ debug@^3.1.0, debug@^3.2.6: dependencies: ms "^2.1.1" +debug@^4.3.1: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2870,6 +2912,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +denque@^1.1.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -3872,6 +3919,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -3935,6 +3987,18 @@ glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -4463,6 +4527,23 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== +ioredis@^4.28.2: + version "4.28.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.2.tgz#493ccd5d869fd0ec86c96498192718171f6c9203" + integrity sha512-kQ+Iv7+c6HsDdPP2XUHaMv8DhnSeAeKEwMbaoqsXYbO+03dItXt7+5jGQDRyjdRUV2rFJbzg7P4Qt1iX2tqkOg== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.3.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + lodash.isarguments "^3.1.0" + p-map "^2.1.0" + redis-commands "1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -4650,6 +4731,14 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-nan@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -5042,6 +5131,21 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -5443,6 +5547,18 @@ mocha@^5.2.0: mkdirp "0.5.1" supports-color "5.4.0" +moment-timezone@^0.5.31: + version "0.5.34" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -5475,12 +5591,27 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpackr-extract@^1.0.14: + version "1.0.16" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.16.tgz#701c4f6e6f25c100ae84557092274e8fffeefe45" + integrity sha512-fxdRfQUxPrL/TizyfYfMn09dK58e+d65bRD/fcaVH4052vj30QOzzqxcQIS7B0NsqlypEQ/6Du3QmP2DhWFfCA== + dependencies: + nan "^2.14.2" + node-gyp-build "^4.2.3" + +msgpackr@^1.4.6: + version "1.5.2" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.5.2.tgz#b400c9885642bdec27b284f8bdadbd6570b448b7" + integrity sha512-OCguCkbG34x1ddO4vAzEm/4J1GTo512k9SoxV8K+EGfI/onFdpemRf0HpsVRFpxadXr4JBFgHsQUitgTlw7ZYQ== + optionalDependencies: + msgpackr-extract "^1.0.14" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@^2.12.1: +nan@^2.12.1, nan@^2.14.2: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== @@ -5546,6 +5677,11 @@ nocache@2.1.0: resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f" integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q== +node-gyp-build@^4.2.3, node-gyp-build@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" + integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== + node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" @@ -5862,6 +5998,11 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-map@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -6583,16 +6724,28 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redis-commands@^1.2.0: +redis-commands@1.7.0, redis-commands@^1.2.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + redis-parser@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs= +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + redis@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" @@ -7023,6 +7176,13 @@ serialize-javascript@^5.0.1: dependencies: randombytes "^2.1.0" +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + serve-static@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" @@ -7292,6 +7452,11 @@ stackframe@^1.1.1: resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303" integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA== +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -7735,7 +7900,7 @@ triple-beam@^1.2.0, triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== -tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.14.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -8034,6 +8199,11 @@ uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -8267,6 +8437,11 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +ws@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6" + integrity sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ== + x-xss-protection@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.3.0.tgz#3e3a8dd638da80421b0e9fff11a2dbe168f6d52c" From 74726f2e43e4e9193d44a3a92ece955242ad1e7c Mon Sep 17 00:00:00 2001 From: alex baldwin Date: Sat, 1 Jan 2022 15:16:48 -0500 Subject: [PATCH 05/42] move find pagination to workers --- src/index.js | 15 +--- src/server/session.js | 34 ++++++++ src/server/tags.js | 32 +++++++ src/server/tumblr.js | 166 ++++++++++++++++--------------------- src/server/workers/find.js | 52 ++++++++++++ 5 files changed, 191 insertions(+), 108 deletions(-) create mode 100644 src/server/session.js diff --git a/src/index.js b/src/index.js index 9d24bb4..a6bd479 100644 --- a/src/index.js +++ b/src/index.js @@ -2,15 +2,13 @@ require('dotenv').config(); const http = require('http'); const express = require('express'); -const Session = require('express-session'); -const RedisStore = require('connect-redis')(Session); const Grant = require('grant-express'); const Sentry = require('@sentry/node'); const Tracing = require('@sentry/tracing'); const helmet = require('helmet'); const WebSocket = require('ws'); -const client = require('./server/redis'); +const { session } = require('./server/session'); const { webRouter, apiRouter } = require('./server/router'); const webSocketHandler = require('./server/websockets'); const logger = require('./server/logger'); @@ -55,17 +53,6 @@ const grant = new Grant({ }, }); -const session = Session({ - store: new RedisStore({ - client: client, - disableTouch: true, - }), - name: 'tagreplacer_session', - resave: false, - secure: process.env.PROTOCOL === 'https', - saveUninitialized: false, - secret: process.env.SECRET, -}); app.use(session); app.use(grant); diff --git a/src/server/session.js b/src/server/session.js new file mode 100644 index 0000000..54a37c9 --- /dev/null +++ b/src/server/session.js @@ -0,0 +1,34 @@ +const Session = require('express-session'); +const RedisStore = require('connect-redis')(Session); + +const client = require('./redis'); + +const sessionStore = new RedisStore({ + client: client, + disableTouch: true, +}); + +const session = Session({ + store: sessionStore, + name: 'tagreplacer_session', + resave: false, + secure: process.env.PROTOCOL === 'https', + saveUninitialized: false, + secret: process.env.SECRET, +}); + +const getSession = sessionId => new Promise((resolve, reject) => ( + sessionStore.get(sessionId, (error, response) => { + if (error) { + reject(error); + } else { + resolve(response); + } + }) +)); + +module.exports = { + session, + sessionStore, + getSession, +}; \ No newline at end of file diff --git a/src/server/tags.js b/src/server/tags.js index f478f0b..79ce44a 100644 --- a/src/server/tags.js +++ b/src/server/tags.js @@ -5,6 +5,13 @@ class Tags { this.caseSensitive = options.caseSensitive || false; this.allowDelete = options.allowDelete || false; } + + /** + * Replaces a set of tags with other tags + * @param {String[]} tags + * @param {String[]} find + * @param {String[]} replace + */ replace({ tags, find, replace }) { /* eslint-disable prefer-const */ let replaceableTags = _.concat([], replace); @@ -40,6 +47,31 @@ class Tags { return result; } + filterPosts({ find, posts }) { + const tags = _.sortBy(find); + + if (this.options.caseSensitive) { + return _.filter(posts, post => { + const sortedPostTags = _.sortBy(post.tags); + return _.isEqual( + _.intersection(sortedPostTags, tags), + tags + ); + }); + } else if (tags.length > 1) { + const lowerCaseTags = _.map(tags, t => t.toLowerCase()); + return _.filter(posts, post => { + const sortedLowerCasePostTags = _.chain(post.tags).map(t => t.toLowerCase()).sortBy().value(); + return _.isEqual( + _.intersection(sortedLowerCasePostTags, lowerCaseTags), + lowerCaseTags, + ); + }); + } else { + return posts; + } + } + } module.exports = Tags; \ No newline at end of file diff --git a/src/server/tumblr.js b/src/server/tumblr.js index 6605338..3fd5b70 100644 --- a/src/server/tumblr.js +++ b/src/server/tumblr.js @@ -86,8 +86,7 @@ class TumblrClient { returnPromises: true })); - const tags = new Tags(options); - this.replaceTags = tags.replace; + this.tags = new Tags(options); this.blog = blog; this.options = _.assign({}, DEFAULT_OPTIONS, options); @@ -134,107 +133,86 @@ class TumblrClient { } /** - * find all posts with a tag and method - * @param {String} tag tag to find - * @param {Method} method method to use - * @return {EventEmitter} + * @typedef Post + * @property id + * @property id_string + * @property post_url + * @property slug + * @property tags + * @property timestamp */ - findPosts({ tag, method }) { - const results = []; - const emitter = new EventEmitter(); - - (async () => { - let next = undefined; - let retry = false; - - while (next !== false) { - try { - const response = await this.client[method.clientMethod](this.blog, { - tag: tag, - limit: POST_LIMIT, - filter: 'text', - [method.nextParam]: next, - }); - - let posts; - if (this.options.caseSensitive) { - posts = _.filter(response.posts, post => _.includes(post.tags, tag)); - } else if (method.key !== METHODS.posts) { - // draft and queue methods don't support the tag param 🙄 - posts = _.filter(response.posts, post => ( - _.includes(post.tags.map(t => t.toLowerCase()), tag.toLowerCase()) - )); - } else { - posts = response.posts; - } - - results.push(...posts); - emitter.emit('data', posts); - - if (_.get(response, '_links.next')) { - next = response._links.next.query_params[method.nextParam]; - await sleep(500); - } else { - next = false; - emitter.emit('end', results); - } - } catch (error) { - // retry once - if (!retry) { - retry = true; - await sleep(500); - } else { - emitter.emit('error', error); - break; - } - } - } - })(); - - return emitter; - } /** - * [findPostsWithTags description] - * @param {MethodName} methodName - * @param {String[]} find - * @return {EventEmitter} + * @typedef FindResponse + * @property {Post[]} posts + * @property {Object} params next page params + * @property {boolean} complete */ - findPostsWithTags(methodName, find) { - if (!_.isArray(find)) throw new Error(`expected 'find' to be an Array, but it was ${typeof find}`); - const emitter = new EventEmitter(); + /** + * find all posts with a tag and method + * @param {Method} methodName method to use + * @param {String[]} tags tags to find + * @param {Object} params api params + * @return {Promise} + */ + async findPostsWithTags(methodName, tags, params = {}) { + const method = METHODS_CONFIG[methodName]; - const tags = _.chain(find) + const sortedTags = _.chain(tags) .sortBy() .value(); - const firstTag = tags[0]; - const method = METHODS_CONFIG[methodName]; - - this.findPosts({ tag: firstTag, method }) - .on('data', (posts) => { - // if multiple tags, filter on results from first tag - if (tags.length > 1) { - const postsWithAllTags = _.filter(posts, post => { - const sortedPostTags = _.sortBy(post.tags); - return _.isEqual( - _.intersection(sortedPostTags, tags), - tags - ); - }); - emitter.emit('data', postsWithAllTags); - } else { - emitter.emit('data', posts); - } - }) - .on('error', error => ( - emitter.emit('error', error) - )) - .on('end', posts => ( - emitter.emit('end', posts) + const firstTag = sortedTags[0]; + + const response = await this.client[method.clientMethod](this.blog, { + tag: firstTag, + limit: POST_LIMIT, + filter: 'text', + ...params, + }); + + let posts; + if (method.key !== METHODS.posts) { + // draft and queue methods don't support the tag param 🙄 + posts = _.filter(response.posts, post => ( + _.includes(post.tags.map(t => t.toLowerCase()), firstTag.toLowerCase()) )); + } else { + posts = response.posts; + } + + if (this.options.caseSensitive) { + posts = _.filter(posts, post => { + const sortedPostTags = _.sortBy(post.tags); + return _.isEqual( + _.intersection(sortedPostTags, tags), + tags + ); + }); + } else if (tags.length > 1) { + const lowerCaseTags = _.map(tags, t => t.toLowerCase()); + posts = _.filter(posts, post => { + const sortedLowerCasePostTags = _.chain(post.tags).map(t => t.toLowerCase()).sortBy().value(); + return _.isEqual( + _.intersection(sortedLowerCasePostTags, lowerCaseTags), + lowerCaseTags, + ); + }); + } - return emitter; + let returnValue = { + posts, + params: {}, + complete: false, + }; + + if (_.get(response, '_links.next')) { + returnValue.params[method.nextParam] = response._links.next.query_params[method.nextParam]; + } else { + returnValue.complete = true; + } + + return returnValue; } /** @@ -255,7 +233,7 @@ class TumblrClient { const promises = posts .slice(0, REPLACE_SOFT_LIMIT) .map(post => { - const replacedTags = this.replaceTags({ + const replacedTags = this.tags.replace({ tags: post.tags, find, replace, diff --git a/src/server/workers/find.js b/src/server/workers/find.js index 2220043..06e50b4 100644 --- a/src/server/workers/find.js +++ b/src/server/workers/find.js @@ -1,3 +1,55 @@ +const { Queue } = require('bullmq'); +const get = require('lodash/get'); + +const { FIND_QUEUE, MESSAGE_QUEUE } = require('../queues'); +const { getSession } = require('../session'); +const TumblrClient = require('../tumblr'); + +const queue = new Queue(FIND_QUEUE); + +/** + * @typedef FindJobData + * @property {String} sessionId + * @property {String} blog + * @property {MethodName} methodName + * @property {String[]} find + * @property {Options} options + * @property {Object} params + */ module.exports = async (job) => { + const { + sessionId, + blog, + methodName, + find, + options, + params, + } = job.data; + + const session = await getSession(sessionId); + const messageQueue = new Queue(MESSAGE_QUEUE(sessionId)); + const token = get(session, 'grant.response.access_token'); + const client = new TumblrClient({ + token, + blog, + options, + }); + + const response = await client.findPostsWithTags(methodName, find, params); + + await messageQueue.add('posts', { + methodName, + blog, + posts: response.posts, + complete: response.complete, + }); + + if (!response.complete) { + await queue.add('find', { + ...job.data, + params: response.params, + }); + } + return; }; \ No newline at end of file From e7956dcf331e4e5c5ba196f4ce99159c4a2e9e2d Mon Sep 17 00:00:00 2001 From: alex baldwin Date: Sat, 1 Jan 2022 16:30:33 -0500 Subject: [PATCH 06/42] update some packages, start converting to hooks --- package.json | 4 +- src/client/components/tagInput.js | 86 +++---- src/client/replacer.js | 2 +- webpack.config.js | 1 + yarn.lock | 405 +++++++++++++----------------- 5 files changed, 218 insertions(+), 280 deletions(-) diff --git a/package.json b/package.json index 4786008..4f91ffc 100644 --- a/package.json +++ b/package.json @@ -83,10 +83,10 @@ "raw-loader": "^0.5.1", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-redux": "^5.1.2", + "react-redux": "^7", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", - "react-select": "^2.4.4", + "react-select": "^4", "redux": "^3.7.2", "redux-thunk": "^2.3.0", "sass-loader": "7.3.1", diff --git a/src/client/components/tagInput.js b/src/client/components/tagInput.js index c03aba2..604d03c 100644 --- a/src/client/components/tagInput.js +++ b/src/client/components/tagInput.js @@ -1,68 +1,50 @@ -import React, { Component } from 'react'; -import autobind from 'class-autobind'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import Creatable from 'react-select/lib/Creatable'; +import { useSelector, useDispatch } from 'react-redux'; +import Creatable from 'react-select/creatable'; import { setFormValue } from '../state/actions'; import { mapForSelect } from '../util'; -const mapStateToProps = (state, ownProps) => ({ - value: state.form[ownProps.name].map(mapForSelect) -}); +const valueRenderer = value => `#${value.label}`; +const noOptionsMessage = () => 'type to add a tag'; +const formatCreateLabel = label => `add #${label}`; +const arrowRenderer = () => null; -const mapDispatchToProps = { - setFormValue: setFormValue, -}; - -class TagInput extends Component { - - constructor(props) { - super(props); - autobind(this); - } +const TagInput = ({ name, disabled, setRef }) => { + const dispatch = useDispatch(); + const value = useSelector(state => state.form[name].map(mapForSelect)); - onChange(value) { - this.props.setFormValue( - this.props.name, + const onChange = useCallback((value) => { + dispatch(setFormValue( + name, value.map(v => v.value) - ); - } - - render() { - const { value, disabled, setRef } = this.props; - - return ( - `#${value.label}`} - isDisabled={disabled} - placeholder="" - noOptionsMessage={() => 'type to add a tag'} - formatCreateLabel={label => `add #${label}`} - arrowRenderer={() => null} - isClearable={false} - className="react-select _specific" - classNamePrefix="react-select" - /> - ); - } - + )); + }, [dispatch, setFormValue, name]); + + return ( + + ); } TagInput.propTypes = { name: PropTypes.string.isRequired, - value: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string, - label: PropTypes.string, - }) - ), disabled: PropTypes.bool, setRef: PropTypes.func, }; -export default connect(mapStateToProps, mapDispatchToProps)(TagInput); +export default React.memo(TagInput); diff --git a/src/client/replacer.js b/src/client/replacer.js index 45474e7..7474a0d 100644 --- a/src/client/replacer.js +++ b/src/client/replacer.js @@ -6,7 +6,7 @@ import { flow, values, map, sum } from 'lodash/fp'; import Options from './options'; import Results from './components/results'; -import TagInput from './components/tagInput'; +import TagInput from './components/TagInput'; import BlogSelect from './components/blogSelect'; import { formatTags } from './util'; diff --git a/webpack.config.js b/webpack.config.js index 8d563b6..f9392c3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -216,6 +216,7 @@ if (PROD) { configFile: path.join(__dirname, 'eslint.js'), useEslintrc: false, cache: false, + emitWarning: true, formatter: require('eslint-formatter-pretty'), }, }], diff --git a/yarn.lock b/yarn.lock index 391f17c..86fe2a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -174,7 +174,7 @@ dependencies: "@babel/types" "^7.15.0" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5": +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3" integrity sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ== @@ -943,13 +943,20 @@ "@babel/plugin-transform-react-jsx-development" "^7.14.5" "@babel/plugin-transform-react-pure-annotations" "^7.14.5" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.8.4": version "7.15.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@7.0.0-beta.44": version "7.0.0-beta.44" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" @@ -1026,52 +1033,70 @@ enabled "2.0.x" kuler "^2.0.0" -"@emotion/babel-utils@^0.6.4": - version "0.6.10" - resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" - integrity sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow== - dependencies: - "@emotion/hash" "^0.6.6" - "@emotion/memoize" "^0.6.6" - "@emotion/serialize" "^0.9.1" - convert-source-map "^1.5.1" - find-root "^1.1.0" - source-map "^0.7.2" - -"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" - integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ== - -"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" - integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ== - -"@emotion/serialize@^0.9.1": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" - integrity sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ== - dependencies: - "@emotion/hash" "^0.6.6" - "@emotion/memoize" "^0.6.6" - "@emotion/unitless" "^0.6.7" - "@emotion/utils" "^0.8.2" - -"@emotion/stylis@^0.7.0": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" - integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ== - -"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": - version "0.6.7" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" - integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg== - -"@emotion/utils@^0.8.2": - version "0.8.2" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" - integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== +"@emotion/cache@^11.4.0", "@emotion/cache@^11.7.1": + version "11.7.1" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539" + integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.1.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "4.0.13" + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/memoize@^0.7.4": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/react@^11.1.1": + version "11.7.1" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07" + integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/cache" "^11.7.1" + "@emotion/serialize" "^1.0.2" + "@emotion/sheet" "^1.1.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" + integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" + integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" + integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + +"@emotion/weak-memoize@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== "@npmcli/move-file@^1.0.1": version "1.1.2" @@ -1149,6 +1174,14 @@ "@sentry/types" "6.11.0" tslib "^1.9.3" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -1159,10 +1192,34 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.1.tgz#c6b9198178da504dfca1fd0be9b2e1002f1586f0" integrity sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A== -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/prop-types@*": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + +"@types/react-redux@^7.1.20": + version "7.1.21" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.21.tgz#f32bbaf7cbc4b93f51e10d340aa54035c084b186" + integrity sha512-bLdglUiBSQNzWVVbmNPKGYYjrzp3/YDPwfOH3nLEz99I4awLlaRAPWjo6bZ2POpxztFWtDDXIPxBLVykXqBt+w== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react@*": + version "17.0.38" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" + integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== "@webassemblyjs/ast@1.9.0": version "1.9.0" @@ -1645,33 +1702,6 @@ babel-plugin-dynamic-import-node@^2.3.3: dependencies: object.assign "^4.1.0" -babel-plugin-emotion@^9.2.11: - version "9.2.11" - resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" - integrity sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@emotion/babel-utils" "^0.6.4" - "@emotion/hash" "^0.6.2" - "@emotion/memoize" "^0.6.1" - "@emotion/stylis" "^0.7.0" - babel-plugin-macros "^2.0.0" - babel-plugin-syntax-jsx "^6.18.0" - convert-source-map "^1.5.0" - find-root "^1.1.0" - mkdirp "^0.5.1" - source-map "^0.5.7" - touch "^2.0.1" - -babel-plugin-macros@^2.0.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" - integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== - dependencies: - "@babel/runtime" "^7.7.2" - cosmiconfig "^6.0.0" - resolve "^1.12.0" - babel-plugin-polyfill-corejs2@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz#e9124785e6fd94f94b618a7954e5693053bf5327" @@ -1696,11 +1726,6 @@ babel-plugin-polyfill-regenerator@^0.2.2: dependencies: "@babel/helper-define-polyfill-provider" "^0.2.2" -babel-plugin-syntax-jsx@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" - integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= - babel-plugin-transform-runtime@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz#88490d446502ea9b8e7efb0fe09ec4d99479b1ee" @@ -2285,7 +2310,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.5, classnames@^2.2.6: +classnames@^2.2.6: version "2.3.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== @@ -2555,7 +2580,7 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.7.0: +convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== @@ -2622,17 +2647,6 @@ cosmiconfig@^5.0.0: js-yaml "^3.13.1" parse-json "^4.0.0" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -2641,19 +2655,6 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.5.3" -create-emotion@^9.2.12: - version "9.2.12" - resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f" - integrity sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA== - dependencies: - "@emotion/hash" "^0.6.2" - "@emotion/memoize" "^0.6.1" - "@emotion/stylis" "^0.7.0" - "@emotion/unitless" "^0.6.2" - csstype "^2.5.2" - stylis "^3.5.0" - stylis-rule-sheet "^0.0.10" - create-error-class@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" @@ -2781,10 +2782,10 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -csstype@^2.5.2: - version "2.6.17" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.17.tgz#4cf30eb87e1d1a005d8b6510f95292413f6a1c0e" - integrity sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A== +csstype@^3.0.2: + version "3.0.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" + integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== cyclist@^1.0.1: version "1.0.1" @@ -2975,12 +2976,13 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" -dom-helpers@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" - integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== dependencies: - "@babel/runtime" "^7.1.2" + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" dom-serializer@^1.0.1: version "1.3.2" @@ -3105,14 +3107,6 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== -emotion@^9.1.2: - version "9.2.12" - resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9" - integrity sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ== - dependencies: - babel-plugin-emotion "^9.2.11" - create-emotion "^9.2.12" - enabled@2.0.x: version "2.0.0" resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" @@ -3730,11 +3724,6 @@ find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-root@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" - integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== - find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -4239,7 +4228,7 @@ hoist-non-react-statics@^2.5.0: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -4411,7 +4400,7 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-fresh@^3.0.0, import-fresh@^3.1.0: +import-fresh@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -4928,11 +4917,6 @@ json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -5041,11 +5025,6 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -6077,16 +6056,6 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -6163,11 +6132,6 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - pathval@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" @@ -6392,6 +6356,15 @@ prop-types@^15.5.1, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, object-assign "^4.1.1" react-is "^16.8.1" +prop-types@^15.7.2: + version "15.8.0" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.0.tgz#d237e624c45a9846e469f5f31117f970017ff588" + integrity sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + proxy-addr@~2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -6509,13 +6482,6 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= -raf@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - random-bytes@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" @@ -6581,22 +6547,22 @@ react-dom@^16.12.0: prop-types "^15.6.2" scheduler "^0.19.1" -react-input-autosize@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2" - integrity sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw== +react-input-autosize@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" + integrity sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg== dependencies: prop-types "^15.5.8" -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-markdown@^2.5.0: version "2.5.1" @@ -6607,18 +6573,17 @@ react-markdown@^2.5.0: commonmark-react-renderer "^4.3.4" prop-types "^15.5.1" -react-redux@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" - integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== +react-redux@^7: + version "7.2.6" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" + integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ== dependencies: - "@babel/runtime" "^7.1.2" - hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" - loose-envify "^1.1.0" - prop-types "^15.6.1" - react-is "^16.6.0" - react-lifecycles-compat "^3.0.0" + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" react-router-dom@^4.3.1: version "4.3.1" @@ -6645,28 +6610,28 @@ react-router@^4.3.1: prop-types "^15.6.1" warning "^4.0.1" -react-select@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.4.4.tgz#ba72468ef1060c7d46fbb862b0748f96491f1f73" - integrity sha512-C4QPLgy9h42J/KkdrpVxNmkY6p4lb49fsrbDk/hRcZpX7JvZPNb6mGj+c5SzyEtBv1DmQ9oPH4NmhAFvCrg8Jw== +react-select@^4: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81" + integrity sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q== dependencies: - classnames "^2.2.5" - emotion "^9.1.2" + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.1.1" memoize-one "^5.0.0" prop-types "^15.6.0" - raf "^3.4.0" - react-input-autosize "^2.2.1" - react-transition-group "^2.2.1" + react-input-autosize "^3.0.0" + react-transition-group "^4.3.0" -react-transition-group@^2.2.1: - version "2.9.0" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" - integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== +react-transition-group@^4.3.0: + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== dependencies: - dom-helpers "^3.4.0" + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" loose-envify "^1.4.0" prop-types "^15.6.2" - react-lifecycles-compat "^3.0.4" react@^16.12.0: version "16.14.0" @@ -6770,6 +6735,13 @@ redux@^3.7.2: loose-envify "^1.1.0" symbol-observable "^1.0.3" +redux@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" + integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + dependencies: + "@babel/runtime" "^7.9.2" + referrer-policy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e" @@ -6964,7 +6936,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2: +resolve@^1.10.0, resolve@^1.14.2: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -7348,7 +7320,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1: +source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.1: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -7358,7 +7330,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.2, source-map@~0.7.2: +source-map@~0.7.2: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -7629,15 +7601,10 @@ style-loader@0.21.0: loader-utils "^1.1.0" schema-utils "^0.4.5" -stylis-rule-sheet@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" - integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== - -stylis@^3.5.0: - version "3.5.4" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" - integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== +stylis@4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" + integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== supports-color@5.4.0: version "5.4.0" @@ -7868,13 +7835,6 @@ toposort@^1.0.0: resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= -touch@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" - integrity sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A== - dependencies: - nopt "~1.0.10" - touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" @@ -8487,11 +8447,6 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" From d41c4b13549fdc6225485ff19f564dff55464c65 Mon Sep 17 00:00:00 2001 From: alex baldwin Date: Sat, 1 Jan 2022 16:36:25 -0500 Subject: [PATCH 07/42] more hooks --- eslint.js | 3 ++- package.json | 1 + .../{blogSelect.js => BlogSelect.js} | 25 ++++++++----------- .../components/{tagInput.js => TagInput.js} | 0 yarn.lock | 5 ++++ 5 files changed, 19 insertions(+), 15 deletions(-) rename src/client/components/{blogSelect.js => BlogSelect.js} (52%) rename src/client/components/{tagInput.js => TagInput.js} (100%) diff --git a/eslint.js b/eslint.js index 8497115..373e182 100644 --- a/eslint.js +++ b/eslint.js @@ -1,10 +1,11 @@ module.exports = { root: true, parser: 'babel-eslint', - plugins: ['jsx-a11y', 'react'], + plugins: ['jsx-a11y', 'react', 'react-hooks'], extends: [ 'eslint:recommended', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', ], diff --git a/package.json b/package.json index 4f91ffc..f8b2a1b 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "eslint-loader": "2.0.0", "eslint-plugin-jsx-a11y": "6.1.1", "eslint-plugin-react": "7.10.0", + "eslint-plugin-react-hooks": "^4.3.0", "file-loader": "^1.1.11", "find-cache-dir": "^2.1.0", "friendly-errors-webpack-plugin": "1.7.0", diff --git a/src/client/components/blogSelect.js b/src/client/components/BlogSelect.js similarity index 52% rename from src/client/components/blogSelect.js rename to src/client/components/BlogSelect.js index 9fa27a4..5d01515 100644 --- a/src/client/components/blogSelect.js +++ b/src/client/components/BlogSelect.js @@ -1,21 +1,19 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import Select from 'react-select'; import { setFormValue } from '../state/actions'; +const BlogSelect = ({ disabled }) => { + const dispatch = useDispatch(); + const blogs = useSelector(state => state.tumblr.blogs); + const value = useSelector(state => state.form.blog); -const mapStateToProps = (state) => ({ - blogs: state.tumblr.blogs, - value: state.form.blog, -}); + const onChange = useCallback(select => ( + dispatch(setFormValue('blog', select.value)) + ), [dispatch]); -const mapDispatchToProps = dispatch => ({ - setFormValue: select => dispatch(setFormValue('blog', select.value)), -}); - -const BlogSelect = ({ blogs, value, disabled, setFormValue }) => { return ( ({ label: blog.name, diff --git a/src/client/components/TagInput.js b/src/client/components/TagInput.js index 604d03c..2078e03 100644 --- a/src/client/components/TagInput.js +++ b/src/client/components/TagInput.js @@ -20,11 +20,12 @@ const TagInput = ({ name, disabled, setRef }) => { name, value.map(v => v.value) )); - }, [dispatch, setFormValue, name]); + }, [dispatch, name]); return ( ({ - blogs: state.tumblr.blogs, - options: state.options, - blog: state.form.blog, - find: state.form.find, - replace: state.form.replace, - foundPosts: ( - state.tumblr.posts?.length > 0 || - state.tumblr.queued?.length > 0 || - state.tumblr.drafts?.length > 0 - ), - loading: state.loading, -}); - -const mapDispatchToProps = { - dispatchFind: find, - dispatchReplace: replace, - dispatchReset: reset, -}; - -class Replacer extends Component { - constructor(props) { - super(props); - - this.state = { - loading: false, - error: undefined, - replaced: [], - }; - - this.inputs = {}; - - autobind(this); - } - - // helpers - - mapForSelect(value) { - return { label: value, value: value }; - } - - find(event) { - if (event) { - event.preventDefault(); - } - - this.props.dispatchFind() - .then(() => { - this.inputs.replace.select.focus(); - }); - } - - replace(event) { - if (event) { - event.preventDefault(); - } - - this.props.dispatchReplace() - .then(action => { - this.setState({ - replaced: action.response, - }); - }); - } - - reset(event) { - if (event) { - event.preventDefault(); - } - - this.props.dispatchReset(); - - this.setState({ - replaced: {}, - }); - } - - // render - - renderReplaced() { - const { find, replace, options } = this.props; - const { replaced } = this.state; - const totalReplaced = flow([ - values, - map('length'), - sum, - ])(replaced); - - if (totalReplaced === 0) return; - - if (replace.length === 0 && options.allowDelete) { - return ( -

- deleted {formatTags(find)} for {totalReplaced} posts -

- ); - } else { - return ( -

- replaced {formatTags(find)} with  - {formatTags(replace)} for  - {totalReplaced} posts -

- ); - } - } - - renderReset() { - if (this.props.foundPosts) { - return ( - - ); - } - } - - render() { - const { loading, foundPosts, options, blog, find, replace } = this.props; - - const disableBlog = !!foundPosts; - const disableFind = !blog || !!foundPosts; - const disableReplace = !foundPosts; - - const disableFindButton = find.length === 0 || disableFind; - const disableReplaceButton = replace.length === 0 && !options.allowDelete; - const deleteMode = replace.length === 0 && options.allowDelete; - - return ( -
- {loading && ( -
-

loading

- -
- )} - -
- - -
- - - - -
- - { - this.inputs.find = ref; - }} - /> - - - -
- - { - this.inputs.replace = ref; - }} - /> - - -
- -
- {foundPosts && ( -
- {this.renderReplaced()} - {this.renderReset()} -
- )} - - {options.includeQueue && } - {options.includeDrafts && } -
-
- ); - } -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Replacer); From bd3c0990d59248f23a7a553dacfb93af15b759d0 Mon Sep 17 00:00:00 2001 From: alex baldwin Date: Sat, 1 Jan 2022 21:05:57 -0500 Subject: [PATCH 10/42] more hooks --- package.json | 2 +- src/client/{help.js => Help.js} | 2 +- src/client/Options.js | 79 +++++++++++++++++++++ src/client/{privacy.js => Privacy.js} | 2 +- src/client/Replacer.js | 10 +-- src/client/app.js | 4 +- src/client/options.js | 98 --------------------------- yarn.lock | 10 +-- 8 files changed, 94 insertions(+), 113 deletions(-) rename src/client/{help.js => Help.js} (91%) create mode 100644 src/client/Options.js rename src/client/{privacy.js => Privacy.js} (91%) delete mode 100644 src/client/options.js diff --git a/package.json b/package.json index f8b2a1b..d955677 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "case-sensitive-paths-webpack-plugin": "2.1.2", "chai": "^4.2.0", "class-autobind": "^0.1.4", - "classnames": "^2.2.6", "clean-webpack-plugin": "^0.1.19", + "clsx": "^1.1.1", "css-loader": "1.0.0", "dotenv": "^2.0.0", "eslint": "^5.16.0", diff --git a/src/client/help.js b/src/client/Help.js similarity index 91% rename from src/client/help.js rename to src/client/Help.js index fa29e11..c13de79 100644 --- a/src/client/help.js +++ b/src/client/Help.js @@ -14,4 +14,4 @@ const Help = () => ( ); -export default Help; +export default React.memo(Help); diff --git a/src/client/Options.js b/src/client/Options.js new file mode 100644 index 0000000..9fa8d64 --- /dev/null +++ b/src/client/Options.js @@ -0,0 +1,79 @@ +import React, { useCallback, useState } from 'react'; +import { useSelector, useDispatch, shallowEqual } from 'react-redux'; + +import { setOption } from './state/actions'; + +import Checkbox from './components/CheckboxInput'; + +// Google Material icon 'tune' +const OptionsIcon = () => ( + + Options + + + +); + +const Options = () => { + const dispatch = useDispatch(); + const { + includeQueue, + includeDrafts, + caseSensitive, + allowDelete, + } = useSelector(state => state.options, shallowEqual); + + const [isOpen, setOpen] = useState(false); + + const toggleOpen = useCallback(() => setOpen(s => !s), []); + const onChange = useCallback((event) => ( + dispatch(setOption(event.target.name, event.target.checked)) + ), [dispatch]); + + return ( +
+ + {isOpen && ( +
+ + + + +
+ )} +
+ ); +}; + +export default React.memo(Options); diff --git a/src/client/privacy.js b/src/client/Privacy.js similarity index 91% rename from src/client/privacy.js rename to src/client/Privacy.js index 0745de0..8d82389 100644 --- a/src/client/privacy.js +++ b/src/client/Privacy.js @@ -14,4 +14,4 @@ const Privacy = () => ( ); -export default Privacy; +export default React.memo(Privacy); diff --git a/src/client/Replacer.js b/src/client/Replacer.js index 6071fc8..c9261f2 100644 --- a/src/client/Replacer.js +++ b/src/client/Replacer.js @@ -1,9 +1,9 @@ import React, { useCallback, useRef, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { flow, values, map, sum } from 'lodash/fp'; -import Options from './options'; +import Options from './Options'; import Results from './components/Results'; import TagInput from './components/TagInput'; import BlogSelect from './components/BlogSelect'; @@ -106,12 +106,12 @@ const Replacer = () => {
-
+ -
+ { -
+