diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 189f09ac0..000000000 --- a/.babelrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "presets": ["next/babel"], - "plugins": [ - [ - "styled-components", - { - "ssr": true, - "displayName": true, - "preprocess": false - } - ] - ] -} diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 7d5c7232d..000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -thirdparty \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 3da5dafd4..f259f7492 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,60 +1,65 @@ { "root": true, - "env": { - "browser": true, - "es2021": true, - "node": true + "parserOptions": { + "project": ["./tsconfig.json"] }, "extends": [ - "plugin:react/recommended", + "next/core-web-vitals", "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "google", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", "prettier" ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 12, - "sourceType": "module" - }, - "plugins": [ - "react", - "@typescript-eslint" - ], + "plugins": ["@typescript-eslint"], + "rules": { - "indent":"off", + "indent": "off", "class-methods-use-this": "off", "react/prop-types": "off", "react/display-name": "off", "react/no-unescaped-entities": "off", "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error" - ], + "@typescript-eslint/no-unused-vars": ["error"], "require-jsdoc": "off", "valid-jsdoc": "off", "max-len": "off", "new-cap": "off", "no-invalid-this": "off", "eqeqeq": "error", - "object-curly-spacing": [ - "error", - "always" - ], + "object-curly-spacing": ["error", "always"], "space-before-function-paren": "off", - "operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }] - }, - "settings": { - "react": { - "version": "detect" - } - }, - "globals": { - "JSX": "readonly", - "NodeJS": "readonly", - "ReadableStreamDefaultController": "readonly" + "operator-linebreak": [ + "error", + "after", + { "overrides": { "?": "before", ":": "before" } } + ], + "import/no-anonymous-default-export": [ + "error", + { + "allowNew": true + } + ], + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/require-await": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + "react-hooks/rules-of-hooks": "off", + "react-hooks/exhaustive-deps": "off", + "@next/next/no-img-element": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "jsx-a11y/alt-text": "off" } } diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af21989..d2ae35e84 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged +yarn lint-staged diff --git a/.lintstagedrc.js b/.lintstagedrc.js new file mode 100644 index 000000000..fa55b0807 --- /dev/null +++ b/.lintstagedrc.js @@ -0,0 +1,13 @@ +const path = require('path'); + +const buildEslintCommand = (filenames) => + `next lint --fix --file ${filenames + .map((f) => path.relative(process.cwd(), f)) + .join(' --file ')}`; + +const buildPrettierCommand = (filenames) => + `yarn prettier --write --ignore-unknown ${filenames.join(' ')}`; + +module.exports = { + '*.{js,jsx,ts,tsx}': [buildEslintCommand, buildPrettierCommand], +}; diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 000000000..02b1010b3 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +network-timeout 500000 diff --git a/README.md b/README.md index ea27599da..a13292474 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,16 @@ **ente** is a cloud storage provider that provides end-to-end encryption for your data. -We have open-source apps across [Android](https://github.com/ente-io/frame), [iOS](https://github.com/ente-io/frame), [web](https://github.com/ente-io/bada-frame) and [desktop](https://github.com/ente-io/bhari-frame) that automatically backup your photos and videos. +We have open-source apps across +[Android](https://github.com/ente-io/photos-app), +[iOS](https://github.com/ente-io/photos-app), +[web](https://github.com/ente-io/photos-web) and +[desktop](https://github.com/ente-io/photos-desktop) that automatically backup +your photos and videos. + +This repository contains the code for our web app, built with a lot of ❤️, and a +little bit of JavaScript. -This repository contains the code for our web app, built with a lot of ❤️, and a little bit of JavaScript.


![App Screenshots](https://user-images.githubusercontent.com/24503581/189914045-9d4e9c44-37c6-4ac6-9e17-d8c37aee1e08.png) @@ -30,7 +37,7 @@ The deployed application is accessible @ [web.ente.io](https://web.ente.io). ## 🧑‍💻 Building from source -1. Clone this repository with `git clone git@github.com:ente-io/bada-frame.git` +1. Clone this repository with `git clone https://github.com/ente-io/photos-web.git` 2. Pull in all submodules with `git submodule update --init --recursive` 3. Install dependencies with `yarn install` 4. Finally, run the development server with `yarn dev` @@ -55,7 +62,8 @@ We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io] If you like this project, please consider upgrading to a paid subscription. -If you would like to motivate us to keep building, you can do so by [starring](https://github.com/ente-io/bada-frame/stargazers) this project. +If you would like to motivate us to keep building, you can do so by +[starring](https://github.com/ente-io/photos-web/stargazers) this project.
@@ -69,5 +77,5 @@ An important part of our journey is to build better software by consistently lis --- -Cross-browser testing provided by +Cross-browser testing provided by [](https://www.browserstack.com/open-source) diff --git a/configUtil.js b/configUtil.js index 274c3d786..65234c2cf 100644 --- a/configUtil.js +++ b/configUtil.js @@ -26,7 +26,7 @@ module.exports = { 'style-src': "'self' 'unsafe-inline'", 'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:", 'connect-src': - "'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ", + "'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/", 'base-uri ': "'self'", // to allow worker 'child-src': "'self' blob:", @@ -37,11 +37,6 @@ module.exports = { 'report-to': ' https://csp-reporter.ente.io/local', }, - WORKBOX_CONFIG: { - swSrc: 'src/serviceWorker.js', - exclude: [/manifest\.json$/i], - }, - ALL_ROUTES: '/(.*)', buildCSPHeader: (directives) => ({ diff --git a/next.config.js b/next.config.js index 5b81c9910..2491e4ee0 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,6 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); -const withWorkbox = require('@ente-io/next-with-workbox'); const { withSentryConfig } = require('@sentry/nextjs'); const { PHASE_DEVELOPMENT_SERVER } = require('next/constants'); @@ -19,7 +18,6 @@ const { COOP_COEP_HEADERS, WEB_SECURITY_HEADERS, CSP_DIRECTIVES, - WORKBOX_CONFIG, ALL_ROUTES, getIsSentryEnabled, } = require('./configUtil'); @@ -30,37 +28,39 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled(); module.exports = (phase) => withSentryConfig( - withWorkbox( - withBundleAnalyzer( - withTM({ - env: { - SENTRY_RELEASE: GIT_SHA, - NEXT_PUBLIC_LATEST_COMMIT_HASH: GIT_SHA, + withBundleAnalyzer( + withTM({ + compiler: { + styledComponents: { + ssr: true, + displayName: true, }, - workbox: WORKBOX_CONFIG, + }, + env: { + SENTRY_RELEASE: GIT_SHA, + }, - headers() { - return [ - { - // Apply these headers to all routes in your application.... - source: ALL_ROUTES, - headers: convertToNextHeaderFormat({ - ...COOP_COEP_HEADERS, - ...WEB_SECURITY_HEADERS, - ...buildCSPHeader(CSP_DIRECTIVES), - }), - }, - ]; - }, - // https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j - webpack: (config, { isServer }) => { - if (!isServer) { - config.resolve.fallback.fs = false; - } - return config; - }, - }) - ) + headers() { + return [ + { + // Apply these headers to all routes in your application.... + source: ALL_ROUTES, + headers: convertToNextHeaderFormat({ + ...COOP_COEP_HEADERS, + ...WEB_SECURITY_HEADERS, + ...buildCSPHeader(CSP_DIRECTIVES), + }), + }, + ]; + }, + // https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j + webpack: (config, { isServer }) => { + if (!isServer) { + config.resolve.fallback.fs = false; + } + return config; + }, + }) ), { release: GIT_SHA, diff --git a/package.json b/package.json index b454d8592..42598d94b 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,7 @@ "scripts": { "dev": "next dev", "albums": "next dev -p 3002", - "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", - "prebuild": "yarn lint", + "lint": "next lint", "build": "next build", "postbuild": "next export", "build-analyze": "ANALYZE=true next build", @@ -15,7 +14,6 @@ }, "dependencies": { "@date-io/date-fns": "^2.14.0", - "@ente-io/next-with-workbox": "^1.0.3", "@mui/icons-material": "^5.6.2", "@mui/material": "^5.6.2", "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest", @@ -53,16 +51,16 @@ "libsodium-wrappers": "^0.7.8", "localforage": "^1.9.0", "ml-matrix": "^6.8.2", - "next": "^12.1.0", - "next-transpile-modules": "^9.0.0", + "next": "^13.0.6", + "next-transpile-modules": "^10.0.0", "p-queue": "^7.1.0", "photoswipe": "file:./thirdparty/photoswipe", "piexifjs": "^1.0.6", - "react": "^17.0.2", + "react": "^18.2.0", "react-bootstrap": "^1.3.0", "react-d3-tree": "^3.1.1", "react-datepicker": "^4.3.0", - "react-dom": "^17.0.2", + "react-dom": "^18.2.0", "react-dropzone": "^11.2.4", "react-otp-input": "^2.3.1", "react-select": "^4.3.1", @@ -70,6 +68,7 @@ "react-top-loading-bar": "^2.0.1", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.8.6", + "sanitize-filename": "^1.6.3", "similarity-transformation": "^0.0.1", "styled-components": "^5.3.5", "tesseract.js": "file:./thirdparty/tesseract", @@ -85,6 +84,7 @@ }, "devDependencies": { "@next/bundle-analyzer": "^9.5.3", + "@types/bs58": "^4.0.1", "@types/debounce-promise": "^3.1.3", "@types/libsodium-wrappers": "^0.7.8", "@types/node": "^14.6.4", @@ -98,17 +98,10 @@ "@types/styled-components": "^5.1.25", "@types/wicg-file-system-access": "^2020.9.5", "@types/yup": "^0.29.7", - "@typescript-eslint/eslint-plugin": "^4.25.0", - "@typescript-eslint/parser": "^4.25.0", - "babel-plugin-styled-components": "^1.11.1", - "eslint": "^7.27.0", - "eslint-config-airbnb": "^18.2.1", - "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.23.3", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.23.2", - "eslint-plugin-react-hooks": "^4.2.0", + "@typescript-eslint/eslint-plugin": "^5.43.0", + "eslint": "^8.28.0", + "eslint-config-next": "^13.0.6", + "eslint-config-prettier": "^8.5.0", "husky": "^7.0.1", "lint-staged": "^11.1.2", "prettier": "2.3.2", @@ -117,13 +110,7 @@ "standard": { "parser": "babel-eslint" }, - "lint-staged": { - "src/**/*.{js,jsx,ts,tsx}": [ - "eslint --fix", - "prettier --write --ignore-unknown" - ] - }, "resolutions": { "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest" } -} +} \ No newline at end of file diff --git a/public/_headers b/public/_headers index 388d4038c..caa299ea2 100644 --- a/public/_headers +++ b/public/_headers @@ -8,5 +8,5 @@ X-Frame-Options: deny X-XSS-Protection: 1; mode=block Referrer-Policy: same-origin - Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io; + Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/ ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io; diff --git a/sentry.client.config.js b/sentry.client.config.js index 3eec2c8fc..aeda578d9 100644 --- a/sentry.client.config.js +++ b/sentry.client.config.js @@ -13,7 +13,6 @@ const SENTRY_ENV = getSentryENV(); const SENTRY_RELEASE = getSentryRelease(); const IS_ENABLED = getIsSentryEnabled(); -Sentry.setUser({ id: getSentryUserID() }); Sentry.init({ dsn: SENTRY_DSN, enabled: IS_ENABLED, @@ -39,3 +38,9 @@ Sentry.init({ // `release` value here - use the environment variable `SENTRY_RELEASE`, so // that it will also get attached to your source maps }); + +const main = async () => { + Sentry.setUser({ id: await getSentryUserID() }); +}; + +main(); diff --git a/sentry.server.config.js b/sentry.server.config.js index 5d8714c99..79b62e6bd 100644 --- a/sentry.server.config.js +++ b/sentry.server.config.js @@ -6,6 +6,8 @@ import { getIsSentryEnabled, } from 'constants/sentry'; +import { getSentryUserID } from 'utils/user'; + const SENTRY_DSN = getSentryDSN(); const SENTRY_ENV = getSentryENV(); const SENTRY_RELEASE = getSentryRelease(); @@ -18,3 +20,9 @@ Sentry.init({ release: SENTRY_RELEASE, autoSessionTracking: false, }); + +const main = async () => { + Sentry.setUser({ id: await getSentryUserID() }); +}; + +main(); diff --git a/sentryConfigUtil.js b/sentryConfigUtil.js index 1b19f2f6b..84d4a7628 100644 --- a/sentryConfigUtil.js +++ b/sentryConfigUtil.js @@ -1,10 +1,11 @@ +const ENV_DEVELOPMENT = 'development'; + module.exports.getIsSentryEnabled = () => { - if (process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED) { - return process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED === 'yes'; + if (process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT) { + return false; + } else if (process.env.NEXT_PUBLIC_DISABLE_SENTRY === 'true') { + return false; } else { - if (process.env.NEXT_PUBLIC_SENTRY_ENV) { - return process.env.NEXT_PUBLIC_SENTRY_ENV !== 'development'; - } + return true; } - return false; }; diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 773729509..c145b2000 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -3,8 +3,9 @@ import { CSSProperties } from '@mui/styled-engine'; export const Badge = styled(Paper)(({ theme }) => ({ padding: '2px 4px', - backgroundColor: theme.palette.glass.main, - color: theme.palette.glass.contrastText, + backgroundColor: theme.palette.backdrop.main, + backdropFilter: `blur(${theme.palette.blur.muted})`, + color: theme.palette.primary.contrastText, textTransform: 'uppercase', ...(theme.typography.mini as CSSProperties), })); diff --git a/src/components/Chip.tsx b/src/components/Chip.tsx new file mode 100644 index 000000000..bce7429e1 --- /dev/null +++ b/src/components/Chip.tsx @@ -0,0 +1,10 @@ +import { Box, styled } from '@mui/material'; +import { CSSProperties } from 'react'; + +export const Chip = styled(Box)(({ theme }) => ({ + ...(theme.typography.body2 as CSSProperties), + padding: '8px 12px', + borderRadius: '4px', + backgroundColor: theme.palette.fill.dark, + fontWeight: 'bold', +})); diff --git a/src/components/CodeBlock/CopyButton.tsx b/src/components/CodeBlock/CopyButton.tsx index 9686c0392..bc8e09f60 100644 --- a/src/components/CodeBlock/CopyButton.tsx +++ b/src/components/CodeBlock/CopyButton.tsx @@ -1,20 +1,43 @@ -import React from 'react'; +import React, { useState } from 'react'; import constants from 'utils/strings/constants'; -import { CopyButtonWrapper } from './styledComponents'; import DoneIcon from '@mui/icons-material/Done'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import { Tooltip } from '@mui/material'; +import { + IconButton, + IconButtonProps, + SvgIconProps, + Tooltip, +} from '@mui/material'; -export default function CopyButton({ code, copied, copyToClipboardHelper }) { +export default function CopyButton({ + code, + color, + size, +}: { + code: string; + color?: IconButtonProps['color']; + size?: SvgIconProps['fontSize']; +}) { + const [copied, setCopied] = useState(false); + + const copyToClipboardHelper = (text: string) => () => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 1000); + }; return ( - - + + {copied ? ( - + ) : ( - + )} - + ); } diff --git a/src/components/CodeBlock/index.tsx b/src/components/CodeBlock/index.tsx index e3e7aff07..a05c913b2 100644 --- a/src/components/CodeBlock/index.tsx +++ b/src/components/CodeBlock/index.tsx @@ -1,7 +1,7 @@ import { FreeFlowText } from '../Container'; -import React, { useState } from 'react'; +import React from 'react'; import EnteSpinner from '../EnteSpinner'; -import { Wrapper, CodeWrapper } from './styledComponents'; +import { Wrapper, CodeWrapper, CopyButtonWrapper } from './styledComponents'; import CopyButton from './CopyButton'; import { BoxProps } from '@mui/material'; @@ -15,14 +15,6 @@ export default function CodeBlock({ wordBreak, ...props }: BoxProps<'div', Iprops>) { - const [copied, setCopied] = useState(false); - - const copyToClipboardHelper = (text: string) => () => { - navigator.clipboard.writeText(text); - setCopied(true); - setTimeout(() => setCopied(false), 1000); - }; - if (!code) { return ( @@ -37,11 +29,9 @@ export default function CodeBlock({ {code} - + + + ); } diff --git a/src/components/Collections/AllCollections/CollectionSort/options.tsx b/src/components/Collections/AllCollections/CollectionSort/options.tsx index 27d0a8b43..e2163342c 100644 --- a/src/components/Collections/AllCollections/CollectionSort/options.tsx +++ b/src/components/Collections/AllCollections/CollectionSort/options.tsx @@ -12,9 +12,6 @@ export default function CollectionSortOptions(props: CollectionSortProps) { {constants.SORT_BY_NAME} - - {constants.SORT_BY_CREATION_TIME_DESCENDING} - {constants.SORT_BY_CREATION_TIME_ASCENDING} diff --git a/src/components/Collections/CollectionInfoWithOptions.tsx b/src/components/Collections/CollectionInfoWithOptions.tsx index e7c85047c..fe09bbce1 100644 --- a/src/components/Collections/CollectionInfoWithOptions.tsx +++ b/src/components/Collections/CollectionInfoWithOptions.tsx @@ -8,8 +8,8 @@ import { CollectionInfoBarWrapper } from './styledComponents'; import { shouldShowOptions } from 'utils/collection'; import { CollectionSummaryType } from 'constants/collection'; import Favorite from '@mui/icons-material/FavoriteRounded'; -import VisibilityOff from '@mui/icons-material/VisibilityOff'; import Delete from '@mui/icons-material/Delete'; +import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined'; interface Iprops { activeCollection: Collection; @@ -43,7 +43,7 @@ export default function CollectionInfoWithOptions({ return ; case CollectionSummaryType.archived: case CollectionSummaryType.archive: - return ; + return ; case CollectionSummaryType.trash: return ; default: diff --git a/src/components/Collections/CollectionListBar/CollectionCard.tsx b/src/components/Collections/CollectionListBar/CollectionCard.tsx index c5bb00df4..b5029ec24 100644 --- a/src/components/Collections/CollectionListBar/CollectionCard.tsx +++ b/src/components/Collections/CollectionListBar/CollectionCard.tsx @@ -11,7 +11,8 @@ import TruncateText from 'components/TruncateText'; import { Box } from '@mui/material'; import { CollectionSummaryType } from 'constants/collection'; import Favorite from '@mui/icons-material/FavoriteRounded'; -import VisibilityOff from '@mui/icons-material/VisibilityOff'; +import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined'; +import PeopleIcon from '@mui/icons-material/People'; interface Iprops { active: boolean; @@ -50,8 +51,9 @@ function CollectionCardIcon({ collectionType }) { {collectionType === CollectionSummaryType.favorites && } {collectionType === CollectionSummaryType.archived && ( - + )} + {collectionType === CollectionSummaryType.shared && } ); } diff --git a/src/components/Collections/CollectionOptions/AlbumCollectionOption.tsx b/src/components/Collections/CollectionOptions/AlbumCollectionOption.tsx index 9d6c4f3c4..10711db0f 100644 --- a/src/components/Collections/CollectionOptions/AlbumCollectionOption.tsx +++ b/src/components/Collections/CollectionOptions/AlbumCollectionOption.tsx @@ -4,11 +4,11 @@ import React from 'react'; import EditIcon from '@mui/icons-material/Edit'; import IosShareIcon from '@mui/icons-material/IosShare'; import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; -import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; -import VisibilityOnOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import constants from 'utils/strings/constants'; import { CollectionActions } from '.'; +import Unarchive from '@mui/icons-material/Unarchive'; +import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined'; interface Iprops { IsArchived: boolean; @@ -53,13 +53,13 @@ export function AlbumCollectionOption({ onClick={handleCollectionAction( CollectionActions.UNARCHIVE )} - startIcon={}> + startIcon={}> {constants.UNARCHIVE} ) : ( }> + startIcon={}> {constants.ARCHIVE} )} diff --git a/src/components/Collections/CollectionOptions/SharedCollectionOption.tsx b/src/components/Collections/CollectionOptions/SharedCollectionOption.tsx new file mode 100644 index 000000000..33d88f0b9 --- /dev/null +++ b/src/components/Collections/CollectionOptions/SharedCollectionOption.tsx @@ -0,0 +1,25 @@ +import { OverflowMenuOption } from 'components/OverflowMenu/option'; +import React from 'react'; +import LogoutIcon from '@mui/icons-material/Logout'; +import constants from 'utils/strings/constants'; +import { CollectionActions } from '.'; + +interface Iprops { + handleCollectionAction: ( + action: CollectionActions, + loader?: boolean + ) => (...args: any[]) => Promise; +} + +export function SharedCollectionOption({ handleCollectionAction }: Iprops) { + return ( + } + onClick={handleCollectionAction( + CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM, + false + )}> + {constants.LEAVE_ALBUM} + + ); +} diff --git a/src/components/Collections/CollectionOptions/index.tsx b/src/components/Collections/CollectionOptions/index.tsx index cfaa96ce1..c58ab1645 100644 --- a/src/components/Collections/CollectionOptions/index.tsx +++ b/src/components/Collections/CollectionOptions/index.tsx @@ -17,6 +17,7 @@ import { AppContext } from 'pages/_app'; import OverflowMenu from 'components/OverflowMenu/menu'; import { CollectionSummaryType } from 'constants/collection'; import { TrashCollectionOption } from './TrashCollectionOption'; +import { SharedCollectionOption } from './SharedCollectionOption'; import MoreHoriz from '@mui/icons-material/MoreHoriz'; interface CollectionOptionsProps { @@ -39,6 +40,8 @@ export enum CollectionActions { SHOW_SHARE_DIALOG, CONFIRM_EMPTY_TRASH, EMPTY_TRASH, + CONFIRM_LEAVE_SHARED_ALBUM, + LEAVE_SHARED_ALBUM, } const CollectionOptions = (props: CollectionOptionsProps) => { @@ -93,6 +96,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => { case CollectionActions.EMPTY_TRASH: callback = emptyTrash; break; + case CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM: + callback = confirmLeaveSharedAlbum; + break; + case CollectionActions.LEAVE_SHARED_ALBUM: + callback = leaveSharedAlbum; + break; default: logError( Error('invalid collection action '), @@ -130,6 +139,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => { redirectToAll(); }; + const leaveSharedAlbum = async () => { + await CollectionAPI.leaveSharedAlbum(activeCollection.id); + redirectToAll(); + }; + const archiveCollection = () => { changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED); }; @@ -200,6 +214,23 @@ const CollectionOptions = (props: CollectionOptionsProps) => { close: { text: constants.CANCEL }, }); + const confirmLeaveSharedAlbum = () => { + setDialogMessage({ + title: constants.LEAVE_SHARED_ALBUM_TITLE, + content: constants.LEAVE_SHARED_ALBUM_MESSAGE, + proceed: { + text: constants.LEAVE_SHARED_ALBUM, + action: handleCollectionAction( + CollectionActions.LEAVE_SHARED_ALBUM + ), + variant: 'danger', + }, + close: { + text: constants.CANCEL, + }, + }); + }; + return ( { + ) : collectionSummaryType === CollectionSummaryType.shared ? ( + ) : ( void; title: string; fromCollection?: number; + onCancel?: () => void; } interface Props { open: boolean; - onClose: (closeBtnClick?: boolean) => void; + onClose: () => void; attributes: CollectionSelectorAttributes; collections: Collection[]; collectionSummaries: CollectionSummaries; @@ -61,15 +62,18 @@ function CollectionSelector({ props.onClose(); }; - const onCloseButtonClick = () => props.onClose(true); + const onUserTriggeredClose = () => { + attributes.onCancel?.(); + props.onClose(); + }; return ( - + {attributes.title} diff --git a/src/components/Collections/CollectionShare/emailShare.tsx b/src/components/Collections/CollectionShare/emailShare.tsx index c40fcdcf2..15a65d3a4 100644 --- a/src/components/Collections/CollectionShare/emailShare.tsx +++ b/src/components/Collections/CollectionShare/emailShare.tsx @@ -5,7 +5,7 @@ import { GalleryContext } from 'pages/gallery'; import React, { useContext } from 'react'; import { shareCollection } from 'services/collectionService'; import { User } from 'types/user'; -import { handleSharingErrors } from 'utils/error'; +import { handleSharingErrors } from 'utils/error/ui'; import { getData, LS_KEYS } from 'utils/storage/localStorage'; import constants from 'utils/strings/constants'; import { CollectionShareSharees } from './sharees'; diff --git a/src/components/Collections/CollectionShare/publicShare/control.tsx b/src/components/Collections/CollectionShare/publicShare/control.tsx index bb744d72f..34e51921c 100644 --- a/src/components/Collections/CollectionShare/publicShare/control.tsx +++ b/src/components/Collections/CollectionShare/publicShare/control.tsx @@ -1,6 +1,5 @@ import { Box, Typography } from '@mui/material'; import { FlexWrapper } from 'components/Container'; -import { ButtonVariant } from 'components/pages/gallery/LinkButton'; import { AppContext } from 'pages/_app'; import React, { useContext, useState } from 'react'; import { @@ -8,7 +7,7 @@ import { deleteShareableURL, } from 'services/collectionService'; import { Collection, PublicURL } from 'types/collection'; -import { handleSharingErrors } from 'utils/error'; +import { handleSharingErrors } from 'utils/error/ui'; import constants from 'utils/strings/constants'; import PublicShareSwitch from './switch'; interface Iprops { @@ -60,7 +59,7 @@ export default function PublicShareControl({ proceed: { text: constants.DISABLE, action: disablePublicSharing, - variant: ButtonVariant.danger, + variant: 'danger', }, }); }; diff --git a/src/components/Collections/CollectionShare/publicShare/manage/deviceLimit.tsx b/src/components/Collections/CollectionShare/publicShare/manage/deviceLimit.tsx index 113683fb8..aad8c1076 100644 --- a/src/components/Collections/CollectionShare/publicShare/manage/deviceLimit.tsx +++ b/src/components/Collections/CollectionShare/publicShare/manage/deviceLimit.tsx @@ -2,15 +2,22 @@ import { Box, Typography } from '@mui/material'; import React from 'react'; import Select from 'react-select'; import { DropdownStyle } from 'styles/dropdown'; +import { Collection, PublicURL, UpdatePublicURL } from 'types/collection'; import { getDeviceLimitOptions } from 'utils/collection'; import constants from 'utils/strings/constants'; import { OptionWithDivider } from './selectComponents/OptionWithDivider'; +interface Iprops { + publicShareProp: PublicURL; + collection: Collection; + updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise; +} + export function ManageDeviceLimit({ publicShareProp, collection, updatePublicShareURLHelper, -}) { +}: Iprops) { const updateDeviceLimit = async (newLimit: number) => { return updatePublicShareURLHelper({ collectionID: collection.id, diff --git a/src/components/Collections/CollectionShare/publicShare/manage/downloadAccess.tsx b/src/components/Collections/CollectionShare/publicShare/manage/downloadAccess.tsx index daa3d4fcc..b21d93c9f 100644 --- a/src/components/Collections/CollectionShare/publicShare/manage/downloadAccess.tsx +++ b/src/components/Collections/CollectionShare/publicShare/manage/downloadAccess.tsx @@ -1,14 +1,21 @@ import { Box, Typography } from '@mui/material'; -import { ButtonVariant } from 'components/pages/gallery/LinkButton'; import { AppContext } from 'pages/_app'; import React, { useContext } from 'react'; +import { PublicURL, Collection, UpdatePublicURL } from 'types/collection'; import constants from 'utils/strings/constants'; import PublicShareSwitch from '../switch'; + +interface Iprops { + publicShareProp: PublicURL; + collection: Collection; + updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise; +} + export function ManageDownloadAccess({ publicShareProp, updatePublicShareURLHelper, collection, -}) { +}: Iprops) { const appContext = useContext(AppContext); const handleFileDownloadSetting = () => { @@ -34,7 +41,7 @@ export function ManageDownloadAccess({ collectionID: collection.id, enableDownload: false, }), - variant: ButtonVariant.danger, + variant: 'danger', }, }); }; @@ -42,7 +49,7 @@ export function ManageDownloadAccess({ {constants.FILE_DOWNLOAD} diff --git a/src/components/Collections/CollectionShare/publicShare/manage/index.tsx b/src/components/Collections/CollectionShare/publicShare/manage/index.tsx index 5216e4eac..f3f4009a7 100644 --- a/src/components/Collections/CollectionShare/publicShare/manage/index.tsx +++ b/src/components/Collections/CollectionShare/publicShare/manage/index.tsx @@ -1,33 +1,37 @@ import { ManageLinkPassword } from './linkPassword'; import { ManageDeviceLimit } from './deviceLimit'; import { ManageLinkExpiry } from './linkExpiry'; -import { PublicLinkSetPassword } from '../setPassword'; import { Stack, Typography } from '@mui/material'; import { GalleryContext } from 'pages/gallery'; import React, { useContext, useState } from 'react'; import { updateShareableURL } from 'services/collectionService'; -import { UpdatePublicURL } from 'types/collection'; +import { Collection, PublicURL, UpdatePublicURL } from 'types/collection'; import { sleep } from 'utils/common'; -import { handleSharingErrors } from 'utils/error'; import constants from 'utils/strings/constants'; import { ManageSectionLabel, ManageSectionOptions, } from '../../styledComponents'; import { ManageDownloadAccess } from './downloadAccess'; +import { handleSharingErrors } from 'utils/error/ui'; +import { SetPublicShareProp } from 'types/publicCollection'; +import { ManagePublicCollect } from './publicCollect'; + +interface Iprops { + publicShareProp: PublicURL; + collection: Collection; + setPublicShareProp: SetPublicShareProp; +} export default function PublicShareManage({ publicShareProp, collection, setPublicShareProp, -}) { +}: Iprops) { const galleryContext = useContext(GalleryContext); - const [changePasswordView, setChangePasswordView] = useState(false); const [sharableLinkError, setSharableLinkError] = useState(null); - const closeConfigurePassword = () => setChangePasswordView(false); - const updatePublicShareURLHelper = async (req: UpdatePublicURL) => { try { galleryContext.setBlockingLoad(true); @@ -73,6 +77,13 @@ export default function PublicShareManage({ updatePublicShareURLHelper } /> + - ); } diff --git a/src/components/Collections/CollectionShare/publicShare/manage/linkExpiry.tsx b/src/components/Collections/CollectionShare/publicShare/manage/linkExpiry.tsx index a7175d206..dcf0a5336 100644 --- a/src/components/Collections/CollectionShare/publicShare/manage/linkExpiry.tsx +++ b/src/components/Collections/CollectionShare/publicShare/manage/linkExpiry.tsx @@ -2,16 +2,23 @@ import { Box, Typography } from '@mui/material'; import React from 'react'; import Select from 'react-select'; import { linkExpiryStyle } from 'styles/linkExpiry'; +import { PublicURL, Collection, UpdatePublicURL } from 'types/collection'; import { shareExpiryOptions } from 'utils/collection'; import constants from 'utils/strings/constants'; -import { dateStringWithMMH } from 'utils/time'; +import { formatDateTime } from 'utils/time/format'; import { OptionWithDivider } from './selectComponents/OptionWithDivider'; +interface Iprops { + publicShareProp: PublicURL; + collection: Collection; + updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise; +} + export function ManageLinkExpiry({ publicShareProp, collection, updatePublicShareURLHelper, -}) { +}: Iprops) { const updateDeviceExpiry = async (optionFn) => { return updatePublicShareURLHelper({ collectionID: collection.id, @@ -31,7 +38,7 @@ export function ManageLinkExpiry({ }} placeholder={ publicShareProp?.validTill - ? dateStringWithMMH(publicShareProp?.validTill) + ? formatDateTime(publicShareProp?.validTill / 1000) : 'never' } onChange={(e) => { diff --git a/src/components/Collections/CollectionShare/publicShare/manage/linkPassword.tsx b/src/components/Collections/CollectionShare/publicShare/manage/linkPassword.tsx deleted file mode 100644 index 21334f20a..000000000 --- a/src/components/Collections/CollectionShare/publicShare/manage/linkPassword.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Box, Typography } from '@mui/material'; -import { ButtonVariant } from 'components/pages/gallery/LinkButton'; -import { AppContext } from 'pages/_app'; -import React, { useContext } from 'react'; -import constants from 'utils/strings/constants'; -import PublicShareSwitch from '../switch'; -export function ManageLinkPassword({ - collection, - publicShareProp, - updatePublicShareURLHelper, - setChangePasswordView, -}) { - const appContext = useContext(AppContext); - - const handlePasswordChangeSetting = async () => { - if (publicShareProp.passwordEnabled) { - await confirmDisablePublicUrlPassword(); - } else { - setChangePasswordView(true); - } - }; - - const confirmDisablePublicUrlPassword = async () => { - appContext.setDialogMessage({ - title: constants.DISABLE_PASSWORD, - content: constants.DISABLE_PASSWORD_MESSAGE, - close: { text: constants.CANCEL }, - proceed: { - text: constants.DISABLE, - action: () => - updatePublicShareURLHelper({ - collectionID: collection.id, - disablePassword: true, - }), - variant: ButtonVariant.danger, - }, - }); - }; - - return ( - - {constants.LINK_PASSWORD_LOCK} - - - ); -} diff --git a/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/index.tsx b/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/index.tsx new file mode 100644 index 000000000..e096c1b89 --- /dev/null +++ b/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/index.tsx @@ -0,0 +1,72 @@ +import { Box, Typography } from '@mui/material'; +import { AppContext } from 'pages/_app'; +import React, { useContext, useState } from 'react'; +import { PublicURL, Collection, UpdatePublicURL } from 'types/collection'; +import constants from 'utils/strings/constants'; +import { PublicLinkSetPassword } from './setPassword'; +import PublicShareSwitch from '../../switch'; + +interface Iprops { + publicShareProp: PublicURL; + collection: Collection; + updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise; +} + +export function ManageLinkPassword({ + collection, + publicShareProp, + updatePublicShareURLHelper, +}: Iprops) { + const appContext = useContext(AppContext); + const [changePasswordView, setChangePasswordView] = useState(false); + + const closeConfigurePassword = () => setChangePasswordView(false); + + const handlePasswordChangeSetting = async () => { + if (publicShareProp.passwordEnabled) { + await confirmDisablePublicUrlPassword(); + } else { + setChangePasswordView(true); + } + }; + + const confirmDisablePublicUrlPassword = async () => { + appContext.setDialogMessage({ + title: constants.DISABLE_PASSWORD, + content: constants.DISABLE_PASSWORD_MESSAGE, + close: { text: constants.CANCEL }, + proceed: { + text: constants.DISABLE, + action: () => + updatePublicShareURLHelper({ + collectionID: collection.id, + disablePassword: true, + }), + variant: 'danger', + }, + }); + }; + + return ( + <> + + + {' '} + {constants.LINK_PASSWORD_LOCK} + + + + + + ); +} diff --git a/src/components/Collections/CollectionShare/publicShare/setPassword.tsx b/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx similarity index 90% rename from src/components/Collections/CollectionShare/publicShare/setPassword.tsx rename to src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx index b1bfd840f..879687ee1 100644 --- a/src/components/Collections/CollectionShare/publicShare/setPassword.tsx +++ b/src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx @@ -3,7 +3,7 @@ import SingleInputForm, { SingleInputFormProps, } from 'components/SingleInputForm'; import React from 'react'; -import CryptoWorker from 'utils/crypto'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; import constants from 'utils/strings/constants'; export function PublicLinkSetPassword({ @@ -28,8 +28,8 @@ export function PublicLinkSetPassword({ }; const enablePublicUrlPassword = async (password: string) => { - const cryptoWorker = await new CryptoWorker(); - const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const kekSalt = await cryptoWorker.generateSaltToDeriveKey(); const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt); return updatePublicShareURLHelper({ diff --git a/src/components/Collections/CollectionShare/publicShare/manage/publicCollect.tsx b/src/components/Collections/CollectionShare/publicShare/manage/publicCollect.tsx new file mode 100644 index 000000000..57418d03f --- /dev/null +++ b/src/components/Collections/CollectionShare/publicShare/manage/publicCollect.tsx @@ -0,0 +1,34 @@ +import { Box, Typography } from '@mui/material'; +import React from 'react'; +import { PublicURL, Collection, UpdatePublicURL } from 'types/collection'; +import constants from 'utils/strings/constants'; +import PublicShareSwitch from '../switch'; + +interface Iprops { + publicShareProp: PublicURL; + collection: Collection; + updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise; +} + +export function ManagePublicCollect({ + publicShareProp, + updatePublicShareURLHelper, + collection, +}: Iprops) { + const handleFileDownloadSetting = () => { + updatePublicShareURLHelper({ + collectionID: collection.id, + enableCollect: !publicShareProp.enableCollect, + }); + }; + + return ( + + {constants.PUBLIC_COLLECT} + + + ); +} diff --git a/src/components/Collections/index.tsx b/src/components/Collections/index.tsx index b60e93e05..7104e1402 100644 --- a/src/components/Collections/index.tsx +++ b/src/components/Collections/index.tsx @@ -96,7 +96,7 @@ export default function Collections(props: Iprops) { itemType: ITEM_TYPE.OTHER, height: 68, }); - }, [collectionSummaries, activeCollectionID]); + }, [collectionSummaries, activeCollectionID, isInSearchMode]); if (shouldBeHidden) { return <>; diff --git a/src/components/DeleteBtn.tsx b/src/components/DeleteBtn.tsx deleted file mode 100644 index df4511cdd..000000000 --- a/src/components/DeleteBtn.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { styled } from '@mui/material'; -import constants from 'utils/strings/constants'; -import { IconWithMessage } from './IconWithMessage'; - -const Wrapper = styled('button')` - border: none; - background-color: #ff6666; - position: fixed; - z-index: 1; - bottom: 30px; - right: 30px; - width: 60px; - height: 60px; - border-radius: 50%; - color: #fff; -`; -export default function DeleteBtn(props) { - return ( - - - - - - - - - ); -} - -DeleteBtn.defaultProps = { - height: 24, - width: 24, - viewBox: '0 0 24 24', -}; diff --git a/src/components/DialogBox/DialogIcon.tsx b/src/components/DialogBox/DialogIcon.tsx new file mode 100644 index 000000000..cebda8d7a --- /dev/null +++ b/src/components/DialogBox/DialogIcon.tsx @@ -0,0 +1,18 @@ +import { Box } from '@mui/material'; +import React from 'react'; + +export default function DialogIcon({ icon }: { icon: React.ReactNode }) { + return ( + + {icon} + + ); +} diff --git a/src/components/DialogBox/base.tsx b/src/components/DialogBox/base.tsx index 395c55cc6..1b4ddf092 100644 --- a/src/components/DialogBox/base.tsx +++ b/src/components/DialogBox/base.tsx @@ -5,6 +5,12 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({ padding: theme.spacing(1, 1.5), maxWidth: '346px', }, + + '& .DialogIcon': { + padding: theme.spacing(2), + paddingBottom: theme.spacing(1), + }, + '& .MuiDialogTitle-root': { padding: theme.spacing(2), paddingBottom: theme.spacing(1), @@ -12,6 +18,11 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({ '& .MuiDialogContent-root': { padding: theme.spacing(2), }, + + '.DialogIcon + .MuiDialogTitle-root': { + paddingTop: 0, + }, + '.MuiDialogTitle-root + .MuiDialogContent-root': { paddingTop: 0, }, diff --git a/src/components/DialogBox/index.tsx b/src/components/DialogBox/index.tsx index 5f70304c6..fce23fd69 100644 --- a/src/components/DialogBox/index.tsx +++ b/src/components/DialogBox/index.tsx @@ -13,6 +13,7 @@ import DialogTitleWithCloseButton, { } from './TitleWithCloseButton'; import DialogBoxBase from './base'; import { DialogBoxAttributes } from 'types/dialogBox'; +import DialogIcon from './DialogIcon'; type IProps = React.PropsWithChildren< Omit & { @@ -48,6 +49,7 @@ export default function DialogBox({ maxWidth={size} onClose={handleClose} {...props}> + {attributes.icon && } {attributes.title && ( ( - - )} + renderInput={() => <>} /> ); diff --git a/src/components/EnteDrawer.tsx b/src/components/EnteDrawer.tsx new file mode 100644 index 000000000..5c860e9d6 --- /dev/null +++ b/src/components/EnteDrawer.tsx @@ -0,0 +1,11 @@ +import { Drawer } from '@mui/material'; +import styled from 'styled-components'; + +export const EnteDrawer = styled(Drawer)(({ theme }) => ({ + '& .MuiPaper-root': { + maxWidth: '375px', + width: '100%', + scrollbarWidth: 'thin', + padding: theme.spacing(1), + }, +})); diff --git a/src/components/ExportFinished.tsx b/src/components/ExportFinished.tsx index 07701b9f2..175d91248 100644 --- a/src/components/ExportFinished.tsx +++ b/src/components/ExportFinished.tsx @@ -1,8 +1,8 @@ import { Button, DialogActions, DialogContent, Stack } from '@mui/material'; import React from 'react'; import { ExportStats } from 'types/export'; -import { formatDateTime } from 'utils/time'; import constants from 'utils/strings/constants'; +import { formatDateTime } from 'utils/time/format'; import { FlexWrapper, Label, Value } from './Container'; import { ComfySpan } from './ExportInProgress'; diff --git a/src/components/FixCreationTime/index.tsx b/src/components/FixCreationTime/index.tsx index 52d7c695c..1e6258053 100644 --- a/src/components/FixCreationTime/index.tsx +++ b/src/components/FixCreationTime/index.tsx @@ -106,7 +106,6 @@ export default function FixCreationTime(props: Props) {
( - - {props.message} - - }> - {props.children} - -); diff --git a/src/components/Navbar/EnteLinkLogo.tsx b/src/components/Navbar/EnteLinkLogo.tsx new file mode 100644 index 000000000..a1add9917 --- /dev/null +++ b/src/components/Navbar/EnteLinkLogo.tsx @@ -0,0 +1,22 @@ +import { Box } from '@mui/material'; +import Ente from 'components/icons/ente'; +import Link from 'next/link'; +import { ENTE_WEBSITE_LINK } from 'constants/urls'; + +export function EnteLinkLogo() { + return ( + + ({ + ':hover': { + cursor: 'pointer', + svg: { + fill: theme.palette.text.secondary, + }, + }, + })}> + + + + ); +} diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index 4bb83142a..416c8570e 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -31,7 +31,7 @@ export default function Notification({ open, onClose, attributes }: Iprops) { }; const handleClick = () => { - attributes.action?.callback(); + attributes.onClick(); onClose(); }; return ( @@ -40,14 +40,15 @@ export default function Notification({ open, onClose, attributes }: Iprops) { anchorOrigin={{ horizontal: 'right', vertical: 'bottom', - }}> + }} + sx={{ backgroundColor: '#000', width: '320px' }}> theme.spacing(1.5, 2), }}> - - {attributes?.icon ?? } + + {attributes.startIcon ?? } - - - {attributes.message}{' '} - - {attributes?.action && ( - - {attributes?.action.text} + + + {attributes.subtext && ( + + {attributes.subtext} )} - - + {attributes.message && ( + + {attributes.message} + + )} + + + {attributes.endIcon ? ( + onClick={attributes.onClick} + sx={{ fontSize: '36px' }}> + {attributes?.endIcon} + + ) : ( + - + )} diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index 11ccc4036..55397283f 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -1,12 +1,12 @@ import { GalleryContext } from 'pages/gallery'; import PreviewCard from './pages/gallery/PreviewCard'; -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { EnteFile } from 'types/file'; import { styled } from '@mui/material'; import DownloadManager from 'services/downloadManager'; import constants from 'utils/strings/constants'; import AutoSizer from 'react-virtualized-auto-sizer'; -import PhotoSwipe from 'components/PhotoSwipe'; +import PhotoViewer from 'components/PhotoViewer'; import { ALL_SECTION, ARCHIVE_SECTION, @@ -15,7 +15,7 @@ import { import { isSharedFile } from 'utils/file'; import { isPlaybackPossible } from 'utils/photoFrame'; import { PhotoList } from './PhotoList'; -import { SetFiles, SelectedState } from 'types/gallery'; +import { SelectedState } from 'types/gallery'; import { FILE_TYPE } from 'constants/file'; import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; @@ -30,6 +30,8 @@ import { logError } from 'utils/sentry'; import { CustomError } from 'utils/error'; import { User } from 'types/user'; import { getData, LS_KEYS } from 'utils/storage/localStorage'; +import { useMemo } from 'react'; +import { Collection } from 'types/collection'; const Container = styled('div')` display: block; @@ -48,7 +50,7 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened'; interface Props { files: EnteFile[]; - setFiles: SetFiles; + collections?: Collection[]; syncWithRemote: () => Promise; favItemIds?: Set; archivedCollections?: Set; @@ -60,7 +62,8 @@ interface Props { openUploader?; isInSearchMode?: boolean; search?: Search; - deleted?: number[]; + deletedFileIds?: Set; + setDeletedFileIds?: (value: Set) => void; activeCollection: number; isSharedCollection?: boolean; enableDownload?: boolean; @@ -69,13 +72,15 @@ interface Props { } type SourceURL = { - imageURL?: string; - videoURL?: string; + originalImageURL?: string; + originalVideoURL?: string; + convertedImageURL?: string; + convertedVideoURL?: string; }; const PhotoFrame = ({ files, - setFiles, + collections, syncWithRemote, favItemIds, archivedCollections, @@ -86,7 +91,8 @@ const PhotoFrame = ({ isInSearchMode, search, resetSearch, - deleted, + deletedFileIds, + setDeletedFileIds, activeCollection, isSharedCollection, enableDownload, @@ -104,75 +110,26 @@ const PhotoFrame = ({ const [rangeStart, setRangeStart] = useState(null); const [currentHover, setCurrentHover] = useState(null); const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); - const filteredDataRef = useRef([]); - const filteredData = filteredDataRef?.current ?? []; const router = useRouter(); const [isSourceLoaded, setIsSourceLoaded] = useState(false); - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Shift') { - setIsShiftKeyPressed(true); - } - }; - const handleKeyUp = (e: KeyboardEvent) => { - if (e.key === 'Shift') { - setIsShiftKeyPressed(false); - } - }; - document.addEventListener('keydown', handleKeyDown, false); - document.addEventListener('keyup', handleKeyUp, false); - router.events.on('hashChangeComplete', (url: string) => { - const start = url.indexOf('#'); - const hash = url.slice(start !== -1 ? start : url.length); - const shouldPhotoSwipeBeOpened = hash.endsWith( - PHOTOSWIPE_HASH_SUFFIX - ); - if (shouldPhotoSwipeBeOpened) { - setOpen(true); - } else { - setOpen(false); - } - }); - return () => { - document.addEventListener('keydown', handleKeyDown, false); - document.addEventListener('keyup', handleKeyUp, false); - }; - }, []); - useEffect(() => { - if (!isNaN(search?.file)) { - const filteredDataIdx = filteredData.findIndex((file) => { - return file.id === search.file; - }); - if (!isNaN(filteredDataIdx)) { - onThumbnailClick(filteredDataIdx)(); - } - resetSearch(); - } - }, [search, filteredData]); - - const resetFetching = () => { - setFetching({}); - }; - - useEffect(() => { - if (selected.count === 0) { - setRangeStart(null); - } - }, [selected]); - - useEffect(() => { + const filteredData = useMemo(() => { const idSet = new Set(); const user: User = getData(LS_KEYS.USER); - filteredDataRef.current = files + + return files .map((item, index) => ({ ...item, dataIndex: index, w: window.innerWidth, h: window.innerHeight, + title: item.pubMagicMetadata?.data.caption, })) .filter((item) => { - if (deleted?.includes(item.id)) { + if ( + deletedFileIds?.has(item.id) && + activeCollection !== TRASH_SECTION + ) { return false; } if ( @@ -236,7 +193,8 @@ const PhotoFrame = ({ activeCollection === ALL_SECTION || activeCollection === ARCHIVE_SECTION || activeCollection === TRASH_SECTION || - activeCollection === item.collectionID + activeCollection === item.collectionID || + isInSearchMode ) { idSet.add(item.id); return true; @@ -245,7 +203,37 @@ const PhotoFrame = ({ } return false; }); - }, [files, deleted, search, activeCollection]); + }, [ + files, + deletedFileIds, + search?.date, + search?.location, + activeCollection, + ]); + + const fileToCollectionsMap = useMemo(() => { + const fileToCollectionsMap = new Map(); + files.forEach((file) => { + if (!fileToCollectionsMap.get(file.id)) { + fileToCollectionsMap.set(file.id, []); + } + fileToCollectionsMap.get(file.id).push(file.collectionID); + }); + return fileToCollectionsMap; + }, [files]); + + const collectionNameMap = useMemo(() => { + if (collections) { + return new Map( + collections.map((collection) => [ + collection.id, + collection.name, + ]) + ); + } else { + return new Map(); + } + }, [collections]); useEffect(() => { const currentURL = new URL(window.location.href); @@ -262,6 +250,59 @@ const PhotoFrame = ({ } }, [open]); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Shift') { + setIsShiftKeyPressed(true); + } + }; + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Shift') { + setIsShiftKeyPressed(false); + } + }; + document.addEventListener('keydown', handleKeyDown, false); + document.addEventListener('keyup', handleKeyUp, false); + router.events.on('hashChangeComplete', (url: string) => { + const start = url.indexOf('#'); + const hash = url.slice(start !== -1 ? start : url.length); + const shouldPhotoSwipeBeOpened = hash.endsWith( + PHOTOSWIPE_HASH_SUFFIX + ); + if (shouldPhotoSwipeBeOpened) { + setOpen(true); + } else { + setOpen(false); + } + }); + return () => { + document.addEventListener('keydown', handleKeyDown, false); + document.addEventListener('keyup', handleKeyUp, false); + }; + }, []); + + useEffect(() => { + if (!isNaN(search?.file)) { + const filteredDataIdx = filteredData.findIndex((file) => { + return file.id === search.file; + }); + if (!isNaN(filteredDataIdx)) { + onThumbnailClick(filteredDataIdx)(); + } + resetSearch(); + } + }, [search, filteredData]); + + const resetFetching = () => { + setFetching({}); + }; + + useEffect(() => { + if (selected.count === 0) { + setRangeStart(null); + } + }, [selected]); + const getFileIndexFromID = (files: EnteFile[], id: number) => { const index = files.findIndex((file) => file.id === id); if (index === -1) { @@ -272,12 +313,10 @@ const PhotoFrame = ({ const updateURL = (id: number) => (url: string) => { const updateFile = (file: EnteFile) => { - file = { - ...file, - msrc: url, - w: window.innerWidth, - h: window.innerHeight, - }; + file.msrc = url; + file.w = window.innerWidth; + file.h = window.innerHeight; + if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) { file.html = `
@@ -307,29 +346,30 @@ const PhotoFrame = ({ } return file; }; - setFiles((files) => { - const index = getFileIndexFromID(files, id); - files[index] = updateFile(files[index]); - return files; - }); const index = getFileIndexFromID(files, id); return updateFile(files[index]); }; const updateSrcURL = async (id: number, srcURL: SourceURL) => { - const { videoURL, imageURL } = srcURL; - const isPlayable = videoURL && (await isPlaybackPossible(videoURL)); + const { + originalImageURL, + convertedImageURL, + originalVideoURL, + convertedVideoURL, + } = srcURL; + const isPlayable = + convertedVideoURL && (await isPlaybackPossible(convertedVideoURL)); const updateFile = (file: EnteFile) => { - file = { - ...file, - w: window.innerWidth, - h: window.innerHeight, - }; + file.w = window.innerWidth; + file.h = window.innerHeight; + file.isSourceLoaded = true; + file.originalImageURL = originalImageURL; + file.originalVideoURL = originalVideoURL; if (file.metadata.fileType === FILE_TYPE.VIDEO) { if (isPlayable) { file.html = ` `; @@ -339,7 +379,7 @@ const PhotoFrame = ({
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD} - Download + Download
`; @@ -348,9 +388,9 @@ const PhotoFrame = ({ if (isPlayable) { file.html = `
- +
@@ -367,15 +407,10 @@ const PhotoFrame = ({ `; } } else { - file.src = imageURL; + file.src = convertedImageURL; } return file; }; - setFiles((files) => { - const index = getFileIndexFromID(files, id); - files[index] = updateFile(files[index]); - return files; - }); setIsSourceLoaded(true); const index = getFileIndexFromID(files, id); return updateFile(files[index]); @@ -441,7 +476,11 @@ const PhotoFrame = ({ handleSelect(filteredData[index].id, index)(!checked); } }; - const getThumbnail = (files: EnteFile[], index: number) => + const getThumbnail = ( + files: EnteFile[], + index: number, + isScrolling: boolean + ) => files[index] ? ( = currentHover && index <= rangeStart) } activeCollection={activeCollection} + showPlaceholder={isScrolling} /> ) : ( <> @@ -499,6 +539,9 @@ const PhotoFrame = ({ item.msrc = newFile.msrc; item.html = newFile.html; item.src = newFile.src; + item.isSourceLoaded = newFile.isSourceLoaded; + item.originalImageURL = newFile.originalImageURL; + item.originalVideoURL = newFile.originalVideoURL; item.w = newFile.w; item.h = newFile.h; @@ -521,10 +564,13 @@ const PhotoFrame = ({ if (!fetching[item.id]) { try { fetching[item.id] = true; - let urls: string[]; + let urls: { original: string[]; converted: string[] }; if (galleryContext.files.has(item.id)) { const mergedURL = galleryContext.files.get(item.id); - urls = mergedURL.split(','); + urls = { + original: mergedURL.original.split(','), + converted: mergedURL.converted.split(','), + }; } else { appContext.startLoading(); if ( @@ -540,26 +586,40 @@ const PhotoFrame = ({ urls = await DownloadManager.getFile(item, true); } appContext.finishLoading(); - const mergedURL = urls.join(','); + const mergedURL = { + original: urls.original.join(','), + converted: urls.converted.join(','), + }; galleryContext.files.set(item.id, mergedURL); } - let imageURL; - let videoURL; + let originalImageURL; + let originalVideoURL; + let convertedImageURL; + let convertedVideoURL; + if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - [imageURL, videoURL] = urls; + [originalImageURL, originalVideoURL] = urls.original; + [convertedImageURL, convertedVideoURL] = urls.converted; } else if (item.metadata.fileType === FILE_TYPE.VIDEO) { - [videoURL] = urls; + [originalVideoURL] = urls.original; + [convertedVideoURL] = urls.converted; } else { - [imageURL] = urls; + [originalImageURL] = urls.original; + [convertedImageURL] = urls.converted; } setIsSourceLoaded(false); const newFile = await updateSrcURL(item.id, { - imageURL, - videoURL, + originalImageURL, + originalVideoURL, + convertedImageURL, + convertedVideoURL, }); item.msrc = newFile.msrc; item.html = newFile.html; item.src = newFile.src; + item.isSourceLoaded = newFile.isSourceLoaded; + item.originalImageURL = newFile.originalImageURL; + item.originalVideoURL = newFile.originalVideoURL; item.w = newFile.w; item.h = newFile.h; try { @@ -606,17 +666,21 @@ const PhotoFrame = ({ /> )} - )} diff --git a/src/components/PhotoList.tsx b/src/components/PhotoList.tsx index 1fd7b7593..0fc6779d5 100644 --- a/src/components/PhotoList.tsx +++ b/src/components/PhotoList.tsx @@ -1,6 +1,6 @@ import React, { useRef, useEffect, useContext } from 'react'; import { VariableSizeList as List } from 'react-window'; -import { Box, styled } from '@mui/material'; +import { Box, Link, styled } from '@mui/material'; import { EnteFile } from 'types/file'; import { IMAGE_CONTAINER_MAX_HEIGHT, @@ -15,17 +15,17 @@ import { import constants from 'utils/strings/constants'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { ENTE_WEBSITE_LINK } from 'constants/urls'; -import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton'; import { convertBytesToHumanReadable } from 'utils/file/size'; import { DeduplicateContext } from 'pages/deduplicate'; import { FlexWrapper } from './Container'; import { Typography } from '@mui/material'; import { GalleryContext } from 'pages/gallery'; import { SpecialPadding } from 'styles/SpecialPadding'; +import { formatDate } from 'utils/time/format'; const A_DAY = 24 * 60 * 60 * 1000; -const NO_OF_PAGES = 2; const FOOTER_HEIGHT = 90; +const ALBUM_FOOTER_HEIGHT = 75; export enum ITEM_TYPE { TIME = 'TIME', @@ -129,7 +129,6 @@ const SizeAndCountContainer = styled(DateContainer)` `; const FooterContainer = styled(ListItemContainer)` - font-size: 14px; margin-bottom: 0.75rem; @media (max-width: 540px) { font-size: 12px; @@ -142,6 +141,13 @@ const FooterContainer = styled(ListItemContainer)` margin-top: calc(2rem + 20px); `; +const AlbumFooterContainer = styled(ListItemContainer)` + margin-top: 48px; + margin-bottom: 10px; + text-align: center; + justify-content: center; +`; + const NothingContainer = styled(ListItemContainer)` color: #979797; text-align: center; @@ -153,7 +159,11 @@ interface Props { width: number; filteredData: EnteFile[]; showAppDownloadBanner: boolean; - getThumbnail: (files: EnteFile[], index: number) => JSX.Element; + getThumbnail: ( + files: EnteFile[], + index: number, + isScrolling?: boolean + ) => JSX.Element; activeCollection: number; resetFetching: () => void; } @@ -223,11 +233,18 @@ export function PhotoList({ if (timeStampList.length === 1) { timeStampList.push(getEmptyListItem()); } + timeStampList.push(getVacuumItem(timeStampList)); + if (publicCollectionGalleryContext.photoListFooter) { + timeStampList.push( + getPhotoListFooter( + publicCollectionGalleryContext.photoListFooter + ) + ); + } if ( showAppDownloadBanner || publicCollectionGalleryContext.accessedThroughSharedURL ) { - timeStampList.push(getVacuumItem(timeStampList)); if (publicCollectionGalleryContext.accessedThroughSharedURL) { timeStampList.push(getAlbumsFooter()); } else { @@ -244,6 +261,11 @@ export function PhotoList({ filteredData, showAppDownloadBanner, publicCollectionGalleryContext.accessedThroughSharedURL, + galleryContext.photoListHeader, + publicCollectionGalleryContext.photoListFooter, + publicCollectionGalleryContext.photoListHeader, + deduplicateContext.isOnDeduplicatePage, + deduplicateContext.fileSizeMap, ]); const groupByFileSize = (timeStampList: TimeStampListItem[]) => { @@ -288,32 +310,27 @@ export function PhotoList({ const groupByTime = (timeStampList: TimeStampListItem[]) => { let listItemIndex = 0; - let currentDate = -1; - + let currentDate; filteredData.forEach((item, index) => { if ( + !currentDate || !isSameDay( new Date(item.metadata.creationTime / 1000), new Date(currentDate) ) ) { currentDate = item.metadata.creationTime / 1000; - const dateTimeFormat = new Intl.DateTimeFormat('en-IN', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - }); + timeStampList.push({ itemType: ITEM_TYPE.TIME, date: isSameDay(new Date(currentDate), new Date()) - ? 'Today' + ? constants.TODAY : isSameDay( new Date(currentDate), new Date(Date.now() - A_DAY) ) - ? 'Yesterday' - : dateTimeFormat.format(currentDate), + ? constants.YESTERDAY + : formatDate(currentDate), id: currentDate.toString(), }); timeStampList.push({ @@ -336,10 +353,13 @@ export function PhotoList({ }); }; - const isSameDay = (first, second) => - first.getFullYear() === second.getFullYear() && - first.getMonth() === second.getMonth() && - first.getDate() === second.getDate(); + const isSameDay = (first, second) => { + return ( + first.getFullYear() === second.getFullYear() && + first.getMonth() === second.getMonth() && + first.getDate() === second.getDate() + ); + }; const getPhotoListHeader = (photoListHeader) => { return { @@ -352,6 +372,17 @@ export function PhotoList({ }; }; + const getPhotoListFooter = (photoListFooter) => { + return { + ...photoListFooter, + item: ( + + {photoListFooter.item} + + ), + }; + }; + const getEmptyListItem = () => { return { itemType: ITEM_TYPE.OTHER, @@ -365,12 +396,17 @@ export function PhotoList({ }; }; const getVacuumItem = (timeStampList) => { + const footerHeight = + publicCollectionGalleryContext.accessedThroughSharedURL + ? ALBUM_FOOTER_HEIGHT + + (publicCollectionGalleryContext.photoListFooter?.height ?? 0) + : FOOTER_HEIGHT; const photoFrameHeight = (() => { let sum = 0; const getCurrentItemSize = getItemSize(timeStampList); for (let i = 0; i < timeStampList.length; i++) { sum += getCurrentItemSize(i); - if (height - sum <= FOOTER_HEIGHT) { + if (height - sum <= footerHeight) { break; } } @@ -379,7 +415,7 @@ export function PhotoList({ return { itemType: ITEM_TYPE.OTHER, item: <>, - height: Math.max(height - photoFrameHeight - FOOTER_HEIGHT, 0), + height: Math.max(height - photoFrameHeight - footerHeight, 0), }; }; @@ -389,7 +425,9 @@ export function PhotoList({ height: FOOTER_HEIGHT, item: ( - {constants.INSTALL_MOBILE_APP()} + + {constants.INSTALL_MOBILE_APP()} + ), }; @@ -398,22 +436,16 @@ export function PhotoList({ const getAlbumsFooter = () => { return { itemType: ITEM_TYPE.OTHER, - height: FOOTER_HEIGHT, + height: ALBUM_FOOTER_HEIGHT, item: ( - -

- {constants.PRESERVED_BY}{' '} - + + + {constants.SHARED_USING}{' '} + {constants.ENTE_IO} - -

-
+ + + ), }; }; @@ -453,9 +485,10 @@ export function PhotoList({ date: currItem.date, span: items[index + 1].items.length, }); - newList[newIndex + 1].items = newList[ - newIndex + 1 - ].items.concat(items[index + 1].items); + newList[newIndex + 1].items = [ + ...newList[newIndex + 1].items, + ...items[index + 1].items, + ]; index += 2; } else { // Adding items would exceed the number of columns. @@ -512,10 +545,6 @@ export function PhotoList({ } }; - const extraRowsToRender = Math.ceil( - (NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT - ); - const generateKey = (index) => { switch (timeStampList[index].itemType) { case ITEM_TYPE.FILE: @@ -527,7 +556,10 @@ export function PhotoList({ } }; - const renderListItem = (listItem: TimeStampListItem) => { + const renderListItem = ( + listItem: TimeStampListItem, + isScrolling: boolean + ) => { switch (listItem.itemType) { case ITEM_TYPE.TIME: return listItem.dates ? ( @@ -553,7 +585,8 @@ export function PhotoList({ const ret = listItem.items.map((item, idx) => getThumbnail( filteredDataCopy, - listItem.itemStartIndex + idx + listItem.itemStartIndex + idx, + isScrolling ) ); if (listItem.groups) { @@ -584,14 +617,15 @@ export function PhotoList({ width={width} itemCount={timeStampList.length} itemKey={generateKey} - overscanCount={extraRowsToRender}> - {({ index, style }) => ( + overscanCount={0} + useIsScrolling> + {({ index, style, isScrolling }) => ( - {renderListItem(timeStampList[index])} + {renderListItem(timeStampList[index], isScrolling)} )} diff --git a/src/components/PhotoSwipe/InfoDialog/ExifData.tsx b/src/components/PhotoSwipe/InfoDialog/ExifData.tsx deleted file mode 100644 index 741ee2086..000000000 --- a/src/components/PhotoSwipe/InfoDialog/ExifData.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useState } from 'react'; -import constants from 'utils/strings/constants'; - -import { RenderInfoItem } from './RenderInfoItem'; -import { LegendContainer } from '../styledComponents/LegendContainer'; -import { Pre } from '../styledComponents/Pre'; -import { - Checkbox, - FormControlLabel, - FormGroup, - Typography, -} from '@mui/material'; - -export function ExifData(props: { exif: any }) { - const { exif } = props; - const [showAll, setShowAll] = useState(false); - - const changeHandler = (e: React.ChangeEvent) => { - setShowAll(e.target.checked); - }; - - const renderAllValues = () =>
{exif.raw}
; - - const renderSelectedValues = () => ( - <> - {exif?.Make && - exif?.Model && - RenderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)} - {exif?.ImageWidth && - exif?.ImageHeight && - RenderInfoItem( - constants.IMAGE_SIZE, - `${exif.ImageWidth} x ${exif.ImageHeight}` - )} - {exif?.Flash && RenderInfoItem(constants.FLASH, exif.Flash)} - {exif?.FocalLength && - RenderInfoItem( - constants.FOCAL_LENGTH, - exif.FocalLength.toString() - )} - {exif?.ApertureValue && - RenderInfoItem( - constants.APERTURE, - exif.ApertureValue.toString() - )} - {exif?.ISOSpeedRatings && - RenderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())} - - ); - - return ( - <> - - - {constants.EXIF} - - - - } - label={constants.SHOW_ALL} - /> - - - {showAll ? renderAllValues() : renderSelectedValues()} - - ); -} diff --git a/src/components/PhotoSwipe/InfoDialog/FileNameEditForm.tsx b/src/components/PhotoSwipe/InfoDialog/FileNameEditForm.tsx deleted file mode 100644 index 5bf96314f..000000000 --- a/src/components/PhotoSwipe/InfoDialog/FileNameEditForm.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useState } from 'react'; -import constants from 'utils/strings/constants'; -import { Col, Form, FormControl } from 'react-bootstrap'; -import { FlexWrapper, Value } from 'components/Container'; -import CloseIcon from '@mui/icons-material/Close'; -import TickIcon from '@mui/icons-material/Done'; -import { Formik } from 'formik'; -import * as Yup from 'yup'; -import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file'; -import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner'; -import { IconButton } from '@mui/material'; - -export interface formValues { - filename: string; -} - -export const FileNameEditForm = ({ - filename, - saveEdits, - discardEdits, - extension, -}) => { - const [loading, setLoading] = useState(false); - - const onSubmit = async (values: formValues) => { - try { - setLoading(true); - await saveEdits(values.filename); - } finally { - setLoading(false); - } - }; - return ( - - initialValues={{ filename }} - validationSchema={Yup.object().shape({ - filename: Yup.string() - .required(constants.REQUIRED) - .max( - MAX_EDITED_FILE_NAME_LENGTH, - constants.FILE_NAME_CHARACTER_LIMIT - ), - })} - validateOnBlur={false} - onSubmit={onSubmit}> - {({ values, errors, handleChange, handleSubmit }) => ( -
- - - - - {errors.filename} - - - {extension && ( - - - {`.${extension}`} - - - )} - - - - {loading ? ( - - ) : ( - - )} - - - - - - - -
- )} - - ); -}; diff --git a/src/components/PhotoSwipe/InfoDialog/RenderFileName.tsx b/src/components/PhotoSwipe/InfoDialog/RenderFileName.tsx deleted file mode 100644 index ea4df744e..000000000 --- a/src/components/PhotoSwipe/InfoDialog/RenderFileName.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, { useState } from 'react'; -import { updateFilePublicMagicMetadata } from 'services/fileService'; -import { EnteFile } from 'types/file'; -import constants from 'utils/strings/constants'; -import { - changeFileName, - splitFilenameAndExtension, - updateExistingFilePubMetadata, -} from 'utils/file'; -import EditIcon from '@mui/icons-material/Edit'; -import { FreeFlowText, Label, Row, Value } from 'components/Container'; -import { logError } from 'utils/sentry'; -import { FileNameEditForm } from './FileNameEditForm'; -import { IconButton } from '@mui/material'; - -export const getFileTitle = (filename, extension) => { - if (extension) { - return filename + '.' + extension; - } else { - return filename; - } -}; - -export function RenderFileName({ - shouldDisableEdits, - file, - scheduleUpdate, -}: { - shouldDisableEdits: boolean; - file: EnteFile; - scheduleUpdate: () => void; -}) { - const originalTitle = file?.metadata.title; - const [isInEditMode, setIsInEditMode] = useState(false); - const [originalFileName, extension] = - splitFilenameAndExtension(originalTitle); - const [filename, setFilename] = useState(originalFileName); - const openEditMode = () => setIsInEditMode(true); - const closeEditMode = () => setIsInEditMode(false); - - const saveEdits = async (newFilename: string) => { - try { - if (file) { - if (filename === newFilename) { - closeEditMode(); - return; - } - setFilename(newFilename); - const newTitle = getFileTitle(newFilename, extension); - let updatedFile = await changeFileName(file, newTitle); - updatedFile = ( - await updateFilePublicMagicMetadata([updatedFile]) - )[0]; - updateExistingFilePubMetadata(file, updatedFile); - scheduleUpdate(); - } - } catch (e) { - logError(e, 'failed to update file name'); - } finally { - closeEditMode(); - } - }; - return ( - <> - - - {!isInEditMode ? ( - <> - - - {getFileTitle(filename, extension)} - - - {!shouldDisableEdits && ( - - - - - - )} - - ) : ( - - )} - - - ); -} diff --git a/src/components/PhotoSwipe/InfoDialog/index.tsx b/src/components/PhotoSwipe/InfoDialog/index.tsx deleted file mode 100644 index b116b60fa..000000000 --- a/src/components/PhotoSwipe/InfoDialog/index.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import constants from 'utils/strings/constants'; -import { formatDateTime } from 'utils/time'; -import { RenderFileName } from './RenderFileName'; -import { ExifData } from './ExifData'; -import { RenderCreationTime } from './RenderCreationTime'; -import { RenderInfoItem } from './RenderInfoItem'; -import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton'; -import { Dialog, DialogContent, Link, styled, Typography } from '@mui/material'; -import { AppContext } from 'pages/_app'; -import { Location, Metadata } from 'types/upload'; -import Photoswipe from 'photoswipe'; -import { getEXIFLocation } from 'services/upload/exifService'; -import { - PhotoPeopleList, - UnidentifiedFaces, -} from 'components/MachineLearning/PeopleList'; - -import { ObjectLabelList } from 'components/MachineLearning/ObjectList'; -import { WordList } from 'components/MachineLearning/WordList'; -import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton'; - -const FileInfoDialog = styled(Dialog)(({ theme }) => ({ - zIndex: 1501, - '& .MuiDialog-container': { - alignItems: 'flex-start', - }, - '& .MuiDialog-paper': { - padding: theme.spacing(2), - }, -})); - -const Legend = styled('span')` - font-size: 20px; - color: #ddd; - display: inline; -`; - -interface Iprops { - shouldDisableEdits: boolean; - showInfo: boolean; - handleCloseInfo: () => void; - items: any[]; - photoSwipe: Photoswipe; - metadata: Metadata; - exif: any; - scheduleUpdate: () => void; -} - -export function FileInfo({ - shouldDisableEdits, - showInfo, - handleCloseInfo, - items, - photoSwipe, - metadata, - exif, - scheduleUpdate, -}: Iprops) { - const appContext = useContext(AppContext); - const [location, setLocation] = useState(null); - const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0); - - useEffect(() => { - if (!location && metadata) { - if (metadata.longitude || metadata.longitude === 0) { - setLocation({ - latitude: metadata.latitude, - longitude: metadata.longitude, - }); - } - } - }, [metadata]); - - useEffect(() => { - if (!location && exif) { - const exifLocation = getEXIFLocation(exif); - if (exifLocation.latitude || exifLocation.latitude === 0) { - setLocation(exifLocation); - } - } - }, [exif]); - - return ( - - - {constants.INFO} - - - - {constants.METADATA} - - - {RenderInfoItem( - constants.FILE_ID, - items[photoSwipe?.getCurrentIndex()]?.id - )} - {metadata?.title && ( - - )} - {metadata?.creationTime && ( - - )} - {metadata?.modificationTime && - RenderInfoItem( - constants.UPDATED_ON, - formatDateTime(metadata.modificationTime / 1000) - )} - {location && - RenderInfoItem( - constants.LOCATION, - - {constants.SHOW_MAP} - - )} - {appContext.mlSearchEnabled && ( - <> -
- {constants.PEOPLE} -
- -
- {constants.UNIDENTIFIED_FACES} -
- -
- {constants.OBJECTS} - -
-
- {constants.TEXT} - -
- - - )} - {exif && ( - <> - - - )} -
-
- ); -} diff --git a/src/components/PhotoSwipe/infoDialog.tsx b/src/components/PhotoSwipe/infoDialog.tsx new file mode 100644 index 000000000..8744c1e11 --- /dev/null +++ b/src/components/PhotoSwipe/infoDialog.tsx @@ -0,0 +1,175 @@ +export {}; // import React, { useContext, useEffect, useState } from 'react'; +// import constants from 'utils/strings/constants'; +// import { formatDateTime } from 'utils/time'; +// import { RenderFileName } from './RenderFileName'; +// import { ExifData } from './ExifData'; +// import { RenderCreationTime } from './RenderCreationTime'; +// import { RenderInfoItem } from './RenderInfoItem'; +// import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton'; +// import { Dialog, DialogContent, Link, styled, Typography } from '@mui/material'; +// import { AppContext } from 'pages/_app'; +// import { Location, Metadata } from 'types/upload'; +// import Photoswipe from 'photoswipe'; +// import { getEXIFLocation } from 'services/upload/exifService'; +// import { +// PhotoPeopleList, +// UnidentifiedFaces, +// } from 'components/MachineLearning/PeopleList'; + +// import { ObjectLabelList } from 'components/MachineLearning/ObjectList'; +// import { WordList } from 'components/MachineLearning/WordList'; +// import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton'; + +// const FileInfoDialog = styled(Dialog)(({ theme }) => ({ +// zIndex: 1501, +// '& .MuiDialog-container': { +// alignItems: 'flex-start', +// }, +// '& .MuiDialog-paper': { +// padding: theme.spacing(2), +// }, +// })); + +// const Legend = styled('span')` +// font-size: 20px; +// color: #ddd; +// display: inline; +// `; + +// interface Iprops { +// shouldDisableEdits: boolean; +// showInfo: boolean; +// handleCloseInfo: () => void; +// items: any[]; +// photoSwipe: Photoswipe; +// metadata: Metadata; +// exif: any; +// scheduleUpdate: () => void; +// } + +// export function FileInfo({ +// shouldDisableEdits, +// showInfo, +// handleCloseInfo, +// items, +// photoSwipe, +// metadata, +// exif, +// scheduleUpdate, +// }: Iprops) { +// const appContext = useContext(AppContext); +// const [location, setLocation] = useState(null); +// const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0); + +// useEffect(() => { +// if (!location && metadata) { +// if (metadata.longitude || metadata.longitude === 0) { +// setLocation({ +// latitude: metadata.latitude, +// longitude: metadata.longitude, +// }); +// } +// } +// }, [metadata]); + +// useEffect(() => { +// if (!location && exif) { +// const exifLocation = getEXIFLocation(exif); +// if (exifLocation.latitude || exifLocation.latitude === 0) { +// setLocation(exifLocation); +// } +// } +// }, [exif]); + +// return ( +// +// +// {constants.INFO} +// +// +// +// {constants.METADATA} +// + +// {RenderInfoItem( +// constants.FILE_ID, +// items[photoSwipe?.getCurrentIndex()]?.id +// )} +// {metadata?.title && ( +// +// )} +// {metadata?.creationTime && ( +// +// )} +// {metadata?.modificationTime && +// RenderInfoItem( +// constants.UPDATED_ON, +// formatDateTime(metadata.modificationTime / 1000) +// )} +// {location && +// RenderInfoItem( +// constants.LOCATION, +// +// {constants.SHOW_MAP} +// +// )} +// {appContext.mlSearchEnabled && ( +// <> +//
+// {constants.PEOPLE} +//
+// +//
+// {constants.UNIDENTIFIED_FACES} +//
+// +//
+// {constants.OBJECTS} +// +//
+//
+// {constants.TEXT} +// +//
+// +// +// )} +// {exif && ( +// <> +// +// +// )} +//
+//
+// ); +// } diff --git a/src/components/PhotoViewer/FileInfo/ExifData.tsx b/src/components/PhotoViewer/FileInfo/ExifData.tsx new file mode 100644 index 000000000..411c90790 --- /dev/null +++ b/src/components/PhotoViewer/FileInfo/ExifData.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import constants from 'utils/strings/constants'; + +import { Stack, styled, Typography } from '@mui/material'; +import { FileInfoSidebar } from '.'; +import Titlebar from 'components/Titlebar'; +import { Box } from '@mui/system'; +import CopyButton from 'components/CodeBlock/CopyButton'; +import { formatDateFull } from 'utils/time/format'; + +const ExifItem = styled(Box)` + padding-left: 8px; + padding-right: 8px; + display: flex; + flex-direction: column; + gap: 4px; +`; + +function parseExifValue(value: any) { + switch (typeof value) { + case 'string': + case 'number': + return value; + default: + if (value instanceof Date) { + return formatDateFull(value); + } + try { + return JSON.stringify(Array.from(value)); + } catch (e) { + return null; + } + } +} +export function ExifData(props: { + exif: any; + open: boolean; + onClose: () => void; + filename: string; + onInfoClose: () => void; +}) { + const { exif, open, onClose, filename, onInfoClose } = props; + + if (!exif) { + return <>; + } + const handleRootClose = () => { + onClose(); + onInfoClose(); + }; + + return ( + + + } + /> + + {[...Object.entries(exif)].map(([key, value]) => + value ? ( + + + {key} + + + {parseExifValue(value)} + + + ) : ( + <> + ) + )} + + + ); +} diff --git a/src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx b/src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx new file mode 100644 index 000000000..1273485bc --- /dev/null +++ b/src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import constants from 'utils/strings/constants'; +import { DialogContent, DialogTitle } from '@mui/material'; +import DialogBoxBase from 'components/DialogBox/base'; +import SingleInputForm, { + SingleInputFormProps, +} from 'components/SingleInputForm'; + +export const FileNameEditDialog = ({ + isInEditMode, + closeEditMode, + filename, + extension, + saveEdits, +}) => { + const onSubmit: SingleInputFormProps['callback'] = async ( + filename, + setFieldError + ) => { + try { + await saveEdits(filename); + closeEditMode(); + } catch (e) { + setFieldError(constants.UNKNOWN_ERROR); + } + }; + return ( + + {constants.RENAME_FILE} + + + + + ); +}; diff --git a/src/components/PhotoViewer/FileInfo/InfoItem.tsx b/src/components/PhotoViewer/FileInfo/InfoItem.tsx new file mode 100644 index 000000000..566316689 --- /dev/null +++ b/src/components/PhotoViewer/FileInfo/InfoItem.tsx @@ -0,0 +1,61 @@ +import Edit from '@mui/icons-material/Edit'; +import { Box, IconButton, Typography } from '@mui/material'; +import { FlexWrapper } from 'components/Container'; +import React from 'react'; +import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner'; + +interface Iprops { + icon: JSX.Element; + title?: string; + caption?: string | JSX.Element; + openEditor?: any; + loading?: boolean; + hideEditOption?: any; + customEndButton?: any; + children?: any; +} + +export default function InfoItem({ + icon, + title, + caption, + openEditor, + loading, + hideEditOption, + customEndButton, + children, +}: Iprops): JSX.Element { + return ( + + + + {icon} + + + {children ? ( + children + ) : ( + <> + + {title} + + + {caption} + + + )} + + + {customEndButton + ? customEndButton + : !hideEditOption && ( + + {!loading ? : } + + )} + + ); +} diff --git a/src/components/PhotoViewer/FileInfo/RenderCaption.tsx b/src/components/PhotoViewer/FileInfo/RenderCaption.tsx new file mode 100644 index 000000000..41a018096 --- /dev/null +++ b/src/components/PhotoViewer/FileInfo/RenderCaption.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { updateFilePublicMagicMetadata } from 'services/fileService'; +import { EnteFile } from 'types/file'; +import { changeCaption, updateExistingFilePubMetadata } from 'utils/file'; +import { logError } from 'utils/sentry'; +import { Box, IconButton, TextField } from '@mui/material'; +import { FlexWrapper } from 'components/Container'; +import { MAX_CAPTION_SIZE } from 'constants/file'; +import { Formik } from 'formik'; +import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner'; +import * as Yup from 'yup'; +import constants from 'utils/strings/constants'; +import Close from '@mui/icons-material/Close'; +import Done from '@mui/icons-material/Done'; + +interface formValues { + caption: string; +} + +export function RenderCaption({ + file, + scheduleUpdate, + refreshPhotoswipe, +}: { + shouldDisableEdits: boolean; + file: EnteFile; + scheduleUpdate: () => void; + refreshPhotoswipe: () => void; +}) { + const [caption, setCaption] = useState( + file?.pubMagicMetadata?.data.caption + ); + + const [loading, setLoading] = useState(false); + + const saveEdits = async (newCaption: string) => { + try { + if (file) { + if (caption === newCaption) { + return; + } + setCaption(newCaption); + + let updatedFile = await changeCaption(file, newCaption); + updatedFile = ( + await updateFilePublicMagicMetadata([updatedFile]) + )[0]; + updateExistingFilePubMetadata(file, updatedFile); + file.title = file.pubMagicMetadata.data.caption; + refreshPhotoswipe(); + scheduleUpdate(); + } + } catch (e) { + logError(e, 'failed to update caption'); + } + }; + + const onSubmit = async (values: formValues) => { + try { + setLoading(true); + await saveEdits(values.caption); + } finally { + setLoading(false); + } + }; + return ( + + + initialValues={{ caption }} + validationSchema={Yup.object().shape({ + caption: Yup.string().max( + MAX_CAPTION_SIZE, + constants.CAPTION_CHARACTER_LIMIT + ), + })} + validateOnBlur={false} + onSubmit={onSubmit}> + {({ + values, + errors, + handleChange, + handleSubmit, + resetForm, + }) => ( +
+ + {values.caption !== caption && ( + + + {loading ? ( + + ) : ( + + )} + + + resetForm({ + values: { caption: caption ?? '' }, + touched: { caption: false }, + }) + } + disabled={loading}> + + + + )} + + )} + +
+ ); +} diff --git a/src/components/PhotoSwipe/InfoDialog/RenderCreationTime.tsx b/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx similarity index 52% rename from src/components/PhotoSwipe/InfoDialog/RenderCreationTime.tsx rename to src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx index 967b4a14f..5a0c6662b 100644 --- a/src/components/PhotoSwipe/InfoDialog/RenderCreationTime.tsx +++ b/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx @@ -1,18 +1,16 @@ import React, { useState } from 'react'; import { updateFilePublicMagicMetadata } from 'services/fileService'; import { EnteFile } from 'types/file'; -import constants from 'utils/strings/constants'; +import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; import { changeFileCreationTime, updateExistingFilePubMetadata, } from 'utils/file'; -import { formatDateTime } from 'utils/time'; -import EditIcon from '@mui/icons-material/Edit'; -import { Label, Row, Value } from 'components/Container'; +import { formatDate, formatTime } from 'utils/time/format'; +import { FlexWrapper } from 'components/Container'; import { logError } from 'utils/sentry'; -import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner'; import EnteDateTimePicker from 'components/EnteDateTimePicker'; -import { IconButton } from '@mui/material'; +import InfoItem from './InfoItem'; export function RenderCreationTime({ shouldDisableEdits, @@ -59,39 +57,24 @@ export function RenderCreationTime({ return ( <> - - - - {isInEditMode ? ( - - ) : ( - formatDateTime(originalCreationTime) - )} - - {!shouldDisableEdits && !isInEditMode && ( - - {loading ? ( - - - - ) : ( - - - - )} - + + } + title={formatDate(originalCreationTime)} + caption={formatTime(originalCreationTime)} + openEditor={openEditMode} + loading={loading} + hideEditOption={shouldDisableEdits || isInEditMode} + /> + {isInEditMode && ( + )} - + ); } diff --git a/src/components/PhotoViewer/FileInfo/RenderFileName.tsx b/src/components/PhotoViewer/FileInfo/RenderFileName.tsx new file mode 100644 index 000000000..f0d9303e7 --- /dev/null +++ b/src/components/PhotoViewer/FileInfo/RenderFileName.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useState } from 'react'; +import { updateFilePublicMagicMetadata } from 'services/fileService'; +import { EnteFile } from 'types/file'; +import { + changeFileName, + splitFilenameAndExtension, + updateExistingFilePubMetadata, +} from 'utils/file'; +import { FlexWrapper } from 'components/Container'; +import { logError } from 'utils/sentry'; +import { FILE_TYPE } from 'constants/file'; +import InfoItem from './InfoItem'; +import { makeHumanReadableStorage } from 'utils/billing'; +import Box from '@mui/material/Box'; +import { FileNameEditDialog } from './FileNameEditDialog'; +import VideocamOutlined from '@mui/icons-material/VideocamOutlined'; +import PhotoOutlined from '@mui/icons-material/PhotoOutlined'; + +const getFileTitle = (filename, extension) => { + if (extension) { + return filename + '.' + extension; + } else { + return filename; + } +}; + +const getCaption = (file: EnteFile, parsedExifData) => { + const megaPixels = parsedExifData?.['megaPixels']; + const resolution = parsedExifData?.['resolution']; + const fileSize = file.info?.fileSize; + + const captionParts = []; + if (megaPixels) { + captionParts.push(megaPixels); + } + if (resolution) { + captionParts.push(resolution); + } + if (fileSize) { + captionParts.push(makeHumanReadableStorage(fileSize)); + } + return ( + + {captionParts.map((caption) => ( + {caption} + ))} + + ); +}; + +export function RenderFileName({ + parsedExifData, + shouldDisableEdits, + file, + scheduleUpdate, +}: { + parsedExifData: Record; + shouldDisableEdits: boolean; + file: EnteFile; + scheduleUpdate: () => void; +}) { + const [isInEditMode, setIsInEditMode] = useState(false); + const openEditMode = () => setIsInEditMode(true); + const closeEditMode = () => setIsInEditMode(false); + const [filename, setFilename] = useState(); + const [extension, setExtension] = useState(); + + useEffect(() => { + const [filename, extension] = splitFilenameAndExtension( + file.metadata.title + ); + setFilename(filename); + setExtension(extension); + }, []); + + const saveEdits = async (newFilename: string) => { + try { + if (file) { + if (filename === newFilename) { + closeEditMode(); + return; + } + setFilename(newFilename); + const newTitle = getFileTitle(newFilename, extension); + let updatedFile = await changeFileName(file, newTitle); + updatedFile = ( + await updateFilePublicMagicMetadata([updatedFile]) + )[0]; + updateExistingFilePubMetadata(file, updatedFile); + scheduleUpdate(); + } + } catch (e) { + logError(e, 'failed to update file name'); + throw e; + } + }; + + return ( + <> + + ) : ( + + ) + } + title={getFileTitle(filename, extension)} + caption={getCaption(file, parsedExifData)} + openEditor={openEditMode} + hideEditOption={shouldDisableEdits || isInEditMode} + /> + + + ); +} diff --git a/src/components/PhotoSwipe/InfoDialog/RenderInfoItem.tsx b/src/components/PhotoViewer/FileInfo/RenderInfoItem.tsx similarity index 100% rename from src/components/PhotoSwipe/InfoDialog/RenderInfoItem.tsx rename to src/components/PhotoViewer/FileInfo/RenderInfoItem.tsx diff --git a/src/components/PhotoViewer/FileInfo/index.tsx b/src/components/PhotoViewer/FileInfo/index.tsx new file mode 100644 index 000000000..9bc8cd134 --- /dev/null +++ b/src/components/PhotoViewer/FileInfo/index.tsx @@ -0,0 +1,329 @@ +import React, { useContext, useEffect, useState } from 'react'; +import constants from 'utils/strings/constants'; +import { RenderFileName } from './RenderFileName'; +import { RenderCreationTime } from './RenderCreationTime'; +import { Box, DialogProps, Link, Stack, styled } from '@mui/material'; +import { Location } from 'types/upload'; +import { getEXIFLocation } from 'services/upload/exifService'; +import { RenderCaption } from './RenderCaption'; + +import CopyButton from 'components/CodeBlock/CopyButton'; +import { formatDate, formatTime } from 'utils/time/format'; +import Titlebar from 'components/Titlebar'; +import InfoItem from './InfoItem'; +import { FlexWrapper } from 'components/Container'; +import EnteSpinner from 'components/EnteSpinner'; +import { EnteFile } from 'types/file'; +import { Chip } from 'components/Chip'; +import LinkButton from 'components/pages/gallery/LinkButton'; +import { ExifData } from './ExifData'; +import { EnteDrawer } from 'components/EnteDrawer'; +import CameraOutlined from '@mui/icons-material/CameraOutlined'; +import LocationOnOutlined from '@mui/icons-material/LocationOnOutlined'; +import TextSnippetOutlined from '@mui/icons-material/TextSnippetOutlined'; +import FolderOutlined from '@mui/icons-material/FolderOutlined'; +import BackupOutlined from '@mui/icons-material/BackupOutlined'; + +import { + PhotoPeopleList, + UnidentifiedFaces, +} from 'components/MachineLearning/PeopleList'; + +import { ObjectLabelList } from 'components/MachineLearning/ObjectList'; +import { WordList } from 'components/MachineLearning/WordList'; +import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton'; +import { AppContext } from 'pages/_app'; +import { Legend } from '../styledComponents/Legend'; + +export const FileInfoSidebar = styled((props: DialogProps) => ( + +))({ + zIndex: 1501, + '& .MuiPaper-root': { + padding: 8, + }, +}); + +interface Iprops { + shouldDisableEdits: boolean; + showInfo: boolean; + handleCloseInfo: () => void; + file: EnteFile; + exif: any; + scheduleUpdate: () => void; + refreshPhotoswipe: () => void; + fileToCollectionsMap: Map; + collectionNameMap: Map; + isTrashCollection: boolean; +} + +function BasicDeviceCamera({ + parsedExifData, +}: { + parsedExifData: Record; +}) { + return ( + + {parsedExifData['fNumber']} + {parsedExifData['exposureTime']} + {parsedExifData['ISO']} + + ); +} + +function getOpenStreetMapLink(location: { + latitude: number; + longitude: number; +}) { + return `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`; +} + +export function FileInfo({ + shouldDisableEdits, + showInfo, + handleCloseInfo, + file, + exif, + scheduleUpdate, + refreshPhotoswipe, + fileToCollectionsMap, + collectionNameMap, + isTrashCollection, +}: Iprops) { + const appContext = useContext(AppContext); + const [location, setLocation] = useState(null); + const [parsedExifData, setParsedExifData] = useState>(); + const [showExif, setShowExif] = useState(false); + const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0); + + const openExif = () => setShowExif(true); + const closeExif = () => setShowExif(false); + + useEffect(() => { + if (!location && file && file.metadata) { + if (file.metadata.longitude || file.metadata.longitude === 0) { + setLocation({ + latitude: file.metadata.latitude, + longitude: file.metadata.longitude, + }); + } + } + }, [file]); + + useEffect(() => { + if (!location && exif) { + const exifLocation = getEXIFLocation(exif); + if (exifLocation.latitude || exifLocation.latitude === 0) { + setLocation(exifLocation); + } + } + }, [exif]); + + useEffect(() => { + if (!exif) { + setParsedExifData({}); + return; + } + const parsedExifData = {}; + if (exif['fNumber']) { + parsedExifData['fNumber'] = `f/${Math.ceil(exif['FNumber'])}`; + } else if (exif['ApertureValue'] && exif['FocalLength']) { + parsedExifData['fNumber'] = `f/${Math.ceil( + exif['FocalLength'] / exif['ApertureValue'] + )}`; + } + const imageWidth = exif['ImageWidth'] ?? exif['ExifImageWidth']; + const imageHeight = exif['ImageHeight'] ?? exif['ExifImageHeight']; + if (imageWidth && imageHeight) { + parsedExifData['resolution'] = `${imageWidth} x ${imageHeight}`; + const megaPixels = Math.round((imageWidth * imageHeight) / 1000000); + if (megaPixels) { + parsedExifData['megaPixels'] = `${Math.round( + (imageWidth * imageHeight) / 1000000 + )}MP`; + } + } + if (exif['Make'] && exif['Model']) { + parsedExifData[ + 'takenOnDevice' + ] = `${exif['Make']} ${exif['Model']}`; + } + if (exif['ExposureTime']) { + parsedExifData['exposureTime'] = `1/${ + 1 / parseFloat(exif['ExposureTime']) + }`; + } + if (exif['ISO']) { + parsedExifData['ISO'] = `ISO${exif['ISO']}`; + } + setParsedExifData(parsedExifData); + }, [exif]); + + if (!file) { + return <>; + } + + return ( + + + + + + + + + {parsedExifData && parsedExifData['takenOnDevice'] && ( + } + title={parsedExifData['takenOnDevice']} + caption={ + + } + hideEditOption + /> + )} + + {location && ( + } + title={constants.LOCATION} + caption={ + + {constants.SHOW_ON_MAP} + + } + customEndButton={ + + } + /> + )} + } + title={constants.DETAILS} + caption={ + typeof exif === 'undefined' ? ( + + ) : exif !== null ? ( + + {constants.VIEW_EXIF} + + ) : ( + constants.NO_EXIF + ) + } + hideEditOption + /> + } + title={formatDate(file.metadata.modificationTime / 1000)} + caption={formatTime(file.metadata.modificationTime / 1000)} + hideEditOption + /> + {!isTrashCollection && ( + } hideEditOption> + + {fileToCollectionsMap + .get(file.id) + ?.filter((collectionID) => + collectionNameMap.has(collectionID) + ) + ?.map((collectionID) => ( + + {collectionNameMap.get(collectionID)} + + ))} + + + )} + {appContext.mlSearchEnabled && ( + <> +
+ {constants.PEOPLE} +
+ +
+ {constants.UNIDENTIFIED_FACES} +
+ +
+ {constants.OBJECTS} + +
+
+ {constants.TEXT} + +
+ + + )} +
+ +
+ ); +} diff --git a/src/components/PhotoSwipe/PhotoSwipe-old.tsx b/src/components/PhotoViewer/PhotoSwipe-old.tsx similarity index 100% rename from src/components/PhotoSwipe/PhotoSwipe-old.tsx rename to src/components/PhotoViewer/PhotoSwipe-old.tsx diff --git a/src/components/PhotoSwipe/index.tsx b/src/components/PhotoViewer/index.tsx similarity index 55% rename from src/components/PhotoSwipe/index.tsx rename to src/components/PhotoViewer/index.tsx index 19a6f6bcb..e1b24da1f 100644 --- a/src/components/PhotoSwipe/index.tsx +++ b/src/components/PhotoViewer/index.tsx @@ -9,26 +9,54 @@ import { import { EnteFile } from 'types/file'; import constants from 'utils/strings/constants'; import exifr from 'exifr'; -import events from './events'; -import { downloadFile } from 'utils/file'; -import { prettyPrintExif } from 'utils/exif'; +import { + downloadFile, + copyFileToClipboard, + getFileExtension, +} from 'utils/file'; import { livePhotoBtnHTML } from 'components/LivePhotoBtn'; import { logError } from 'utils/sentry'; import { FILE_TYPE } from 'constants/file'; -import { sleep } from 'utils/common'; +import { isClipboardItemPresent } from 'utils/common'; import { playVideo, pauseVideo } from 'utils/photoFrame'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { AppContext } from 'pages/_app'; -import { FileInfo } from './InfoDialog'; -import { defaultLivePhotoDefaultOptions } from 'constants/photoswipe'; +import { FileInfo } from './FileInfo'; +import { + defaultLivePhotoDefaultOptions, + photoSwipeV4Events, +} from 'constants/photoViewer'; import { LivePhotoBtn } from './styledComponents/LivePhotoBtn'; import DownloadIcon from '@mui/icons-material/Download'; import InfoIcon from '@mui/icons-material/InfoOutlined'; import FavoriteIcon from '@mui/icons-material/FavoriteRounded'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorderRounded'; import ChevronRight from '@mui/icons-material/ChevronRight'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { trashFiles } from 'services/fileService'; +import { getTrashFileMessage } from 'utils/ui'; +import { styled } from '@mui/material'; +import { addLocalLog } from 'utils/logging'; +import ContentCopy from '@mui/icons-material/ContentCopy'; +import ChevronLeft from '@mui/icons-material/ChevronLeft'; +interface PhotoswipeFullscreenAPI { + enter: () => void; + exit: () => void; + isFullscreen: () => boolean; +} + +const CaptionContainer = styled('div')(({ theme }) => ({ + padding: theme.spacing(2), + wordBreak: 'break-word', + textAlign: 'right', + maxWidth: '375px', + fontSize: '14px', + lineHeight: '17px', + backgroundColor: theme.palette.backdrop.light, + backdropFilter: `blur(${theme.palette.blur.base})`, +})); interface Iprops { isOpen: boolean; items: any[]; @@ -38,13 +66,17 @@ interface Iprops { id?: string; className?: string; favItemIds: Set; + deletedFileIds: Set; + setDeletedFileIds?: (value: Set) => void; isSharedCollection: boolean; isTrashCollection: boolean; enableDownload: boolean; isSourceLoaded: boolean; + fileToCollectionsMap: Map; + collectionNameMap: Map; } -function PhotoSwipe(props: Iprops) { +function PhotoViewer(props: Iprops) { const pswpElement = useRef(); const [photoSwipe, setPhotoSwipe] = useState>(); @@ -52,8 +84,9 @@ function PhotoSwipe(props: Iprops) { const { isOpen, items, isSourceLoaded } = props; const [isFav, setIsFav] = useState(false); const [showInfo, setShowInfo] = useState(false); - const [metadata, setMetaData] = useState(null); - const [exif, setExif] = useState(null); + const [exif, setExif] = + useState<{ key: string; value: Record }>(); + const exifCopy = useRef(null); const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState( defaultLivePhotoDefaultOptions ); @@ -63,6 +96,9 @@ function PhotoSwipe(props: Iprops) { ); const appContext = useContext(AppContext); + const exifExtractionInProgress = useRef(null); + const [shouldShowCopyOption] = useState(isClipboardItemPresent()); + useEffect(() => { if (!pswpElement) return; if (isOpen) { @@ -76,6 +112,57 @@ function PhotoSwipe(props: Iprops) { }; }, [isOpen]); + useEffect(() => { + if (!photoSwipe) return; + function handleCopyEvent() { + copyToClipboardHelper(photoSwipe.currItem as EnteFile); + } + + function handleKeyUp(event: KeyboardEvent) { + if (!isOpen || showInfo) { + return; + } + + addLocalLog(() => 'Event: ' + event.key); + + switch (event.key) { + case 'i': + case 'I': + setShowInfo(true); + break; + case 'Backspace': + case 'Delete': + confirmTrashFile(photoSwipe?.currItem as EnteFile); + break; + case 'd': + case 'D': + downloadFileHelper(photoSwipe?.currItem as EnteFile); + break; + case 'f': + case 'F': + toggleFullscreen(photoSwipe); + break; + case 'l': + case 'L': + onFavClick(photoSwipe?.currItem as EnteFile); + break; + default: + break; + } + } + + window.addEventListener('keyup', handleKeyUp); + if (shouldShowCopyOption) { + window.addEventListener('copy', handleCopyEvent); + } + return () => { + window.removeEventListener('keyup', handleKeyUp); + if (shouldShowCopyOption) { + window.removeEventListener('copy', handleCopyEvent); + } + }; + }, [isOpen, photoSwipe, showInfo]); + useEffect(() => { updateItems(items); }, [items]); @@ -152,8 +239,12 @@ function PhotoSwipe(props: Iprops) { } }, [photoSwipe?.currItem, isOpen, isSourceLoaded]); - function updateFavButton() { - setIsFav(isInFav(this?.currItem)); + useEffect(() => { + exifCopy.current = exif; + }, [exif]); + + function updateFavButton(file: EnteFile) { + setIsFav(isInFav(file)); } const openPhotoSwipe = () => { @@ -198,12 +289,12 @@ function PhotoSwipe(props: Iprops) { items, options ); - events.forEach((event) => { + photoSwipeV4Events.forEach((event) => { const callback = props[event]; if (callback || event === 'destroy') { photoSwipe.listen(event, function (...args) { if (callback) { - args.unshift(this); + args.unshift(photoSwipe); callback(...args); } if (event === 'destroy') { @@ -215,11 +306,39 @@ function PhotoSwipe(props: Iprops) { }); } }); - photoSwipe.listen('beforeChange', function () { - updateInfo.call(this); - updateFavButton.call(this); + photoSwipe.listen('beforeChange', () => { + const currItem = photoSwipe?.currItem as EnteFile; + updateFavButton(currItem); + if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) { + setExif({ key: currItem.src, value: null }); + return; + } + if ( + !currItem || + !exifCopy?.current?.value === null || + exifCopy?.current?.key === currItem.src + ) { + return; + } + setExif({ key: currItem.src, value: undefined }); + checkExifAvailable(currItem); + }); + photoSwipe.listen('resize', () => { + const currItem = photoSwipe?.currItem as EnteFile; + if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) { + setExif({ key: currItem.src, value: null }); + return; + } + if ( + !currItem || + !exifCopy?.current?.value === null || + exifCopy?.current?.key === currItem.src + ) { + return; + } + setExif({ key: currItem.src, value: undefined }); + checkExifAvailable(currItem); }); - photoSwipe.listen('resize', checkExifAvailable); photoSwipe.init(); needUpdate.current = false; setPhotoSwipe(photoSwipe); @@ -240,7 +359,7 @@ function PhotoSwipe(props: Iprops) { } handleCloseInfo(); }; - const isInFav = (file) => { + const isInFav = (file: EnteFile) => { const { favItemIds } = props; if (favItemIds && file) { return favItemIds.has(file.id); @@ -248,7 +367,7 @@ function PhotoSwipe(props: Iprops) { return false; }; - const onFavClick = async (file) => { + const onFavClick = async (file: EnteFile) => { const { favItemIds } = props; if (!isInFav(file)) { favItemIds.add(file.id); @@ -262,46 +381,80 @@ function PhotoSwipe(props: Iprops) { needUpdate.current = true; }; + const trashFile = async (file: EnteFile) => { + const { deletedFileIds, setDeletedFileIds } = props; + deletedFileIds.add(file.id); + setDeletedFileIds(new Set(deletedFileIds)); + await trashFiles([file]); + needUpdate.current = true; + }; + + const confirmTrashFile = (file: EnteFile) => { + if (props.isSharedCollection || props.isTrashCollection) { + return; + } + appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file))); + }; + const updateItems = (items = []) => { if (photoSwipe) { + if (items.length === 0) { + photoSwipe.close(); + } photoSwipe.items.length = 0; items.forEach((item) => { photoSwipe.items.push(item); }); photoSwipe.invalidateCurrItems(); - // photoSwipe.updateSize(true); + if (isOpen) { + photoSwipe.updateSize(true); + if (photoSwipe.getCurrentIndex() >= photoSwipe.items.length) { + photoSwipe.goTo(0); + } + } } }; - const checkExifAvailable = async () => { - setExif(null); - await sleep(100); + const refreshPhotoswipe = () => { + photoSwipe.invalidateCurrItems(); + if (isOpen) { + photoSwipe.updateSize(true); + } + }; + + const checkExifAvailable = async (file: EnteFile) => { try { - const img: HTMLImageElement = document.querySelector( - '.pswp__img:not(.pswp__img--placeholder)' - ); - if (img) { - const exifData = await exifr.parse(img); - if (!exifData) { - return; + if (exifExtractionInProgress.current === file.src) { + return; + } + try { + if (file.isSourceLoaded) { + exifExtractionInProgress.current = file.src; + const imageBlob = await ( + await fetch(file.originalImageURL) + ).blob(); + const exifData = (await exifr.parse(imageBlob)) as Record< + string, + any + >; + if (exifExtractionInProgress.current === file.src) { + if (exifData) { + setExif({ key: file.src, value: exifData }); + } else { + setExif({ key: file.src, value: null }); + } + } } - exifData.raw = prettyPrintExif(exifData); - setExif(exifData); + } finally { + exifExtractionInProgress.current = null; } } catch (e) { - logError(e, 'exifr parsing failed'); + setExif({ key: file.src, value: null }); + const fileExtension = getFileExtension(file.metadata.title); + logError(e, 'exifr parsing failed', { extension: fileExtension }); } }; - function updateInfo() { - const file: EnteFile = this?.currItem; - if (file?.metadata) { - setMetaData(file.metadata); - setExif(null); - checkExifAvailable(); - } - } - const handleCloseInfo = () => { setShowInfo(false); }; @@ -310,15 +463,37 @@ function PhotoSwipe(props: Iprops) { }; const downloadFileHelper = async (file) => { - appContext.startLoading(); - await downloadFile( - file, - publicCollectionGalleryContext.accessedThroughSharedURL, - publicCollectionGalleryContext.token, - publicCollectionGalleryContext.passwordToken - ); + if (props.enableDownload) { + appContext.startLoading(); + await downloadFile( + file, + publicCollectionGalleryContext.accessedThroughSharedURL, + publicCollectionGalleryContext.token, + publicCollectionGalleryContext.passwordToken + ); + appContext.finishLoading(); + } + }; - appContext.finishLoading(); + const copyToClipboardHelper = async (file: EnteFile) => { + if (props.enableDownload && shouldShowCopyOption) { + appContext.startLoading(); + await copyFileToClipboard(file.src); + appContext.finishLoading(); + } + }; + + const toggleFullscreen = (photoSwipe) => { + const fullScreenApi: PhotoswipeFullscreenAPI = + photoSwipe?.ui?.getFullscreenAPI(); + if (!fullScreenApi) { + return; + } + if (fullScreenApi.isFullscreen()) { + fullScreenApi.exit(); + } else { + fullScreenApi.enter(); + } }; const scheduleUpdate = () => (needUpdate.current = true); const { id } = props; @@ -355,33 +530,74 @@ function PhotoSwipe(props: Iprops) { )} - + )} {!props.isSharedCollection && !props.isTrashCollection && ( + )} + + )} + {!props.isSharedCollection && + !props.isTrashCollection && ( + )} - {!props.isSharedCollection && ( - - )} +
@@ -411,36 +620,34 @@ function PhotoSwipe(props: Iprops) {
-
-
+
+
); } -export default PhotoSwipe; +export default PhotoViewer; diff --git a/src/components/PhotoSwipe/styledComponents/Legend.tsx b/src/components/PhotoViewer/styledComponents/Legend.tsx similarity index 100% rename from src/components/PhotoSwipe/styledComponents/Legend.tsx rename to src/components/PhotoViewer/styledComponents/Legend.tsx diff --git a/src/components/PhotoSwipe/styledComponents/LegendContainer.tsx b/src/components/PhotoViewer/styledComponents/LegendContainer.tsx similarity index 100% rename from src/components/PhotoSwipe/styledComponents/LegendContainer.tsx rename to src/components/PhotoViewer/styledComponents/LegendContainer.tsx diff --git a/src/components/PhotoSwipe/styledComponents/LivePhotoBtn.tsx b/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx similarity index 100% rename from src/components/PhotoSwipe/styledComponents/LivePhotoBtn.tsx rename to src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx diff --git a/src/components/PhotoSwipe/styledComponents/Pre.tsx b/src/components/PhotoViewer/styledComponents/Pre.tsx similarity index 100% rename from src/components/PhotoSwipe/styledComponents/Pre.tsx rename to src/components/PhotoViewer/styledComponents/Pre.tsx diff --git a/src/components/PhotoSwipe/styledComponents/SmallLoadingSpinner.tsx b/src/components/PhotoViewer/styledComponents/SmallLoadingSpinner.tsx similarity index 100% rename from src/components/PhotoSwipe/styledComponents/SmallLoadingSpinner.tsx rename to src/components/PhotoViewer/styledComponents/SmallLoadingSpinner.tsx diff --git a/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx b/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx index eaa603813..ed3fae118 100644 --- a/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx +++ b/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx @@ -1,6 +1,5 @@ import React, { useContext } from 'react'; import { PeopleList } from 'components/MachineLearning/PeopleList'; -import { Legend } from 'components/PhotoSwipe/styledComponents/Legend'; import { IndexStatus } from 'types/machineLearning/ui'; import { SuggestionType, Suggestion } from 'types/search'; import { components } from 'react-select'; @@ -18,6 +17,12 @@ const LegendRow = styled(Row)` margin-bottom: 0px; `; +const Legend = styled('span')` + font-size: 20px; + color: #ddd; + display: inline; +`; + const Caption = styled('span')` font-size: 12px; display: inline; diff --git a/src/components/Search/SearchBar/searchInput/index.tsx b/src/components/Search/SearchBar/searchInput/index.tsx index 32292685d..ec5d3182e 100644 --- a/src/components/Search/SearchBar/searchInput/index.tsx +++ b/src/components/Search/SearchBar/searchInput/index.tsx @@ -44,7 +44,9 @@ export default function SearchInput(props: Iprops) { }; const [defaultOptions, setDefaultOptions] = useState([]); - useEffect(() => search(value), [value]); + useEffect(() => { + search(value); + }, [value]); useEffect(() => { refreshDefaultOptions(); diff --git a/src/components/Sidebar/DebugLogs.tsx b/src/components/Sidebar/DebugLogs.tsx deleted file mode 100644 index 2f4283d80..000000000 --- a/src/components/Sidebar/DebugLogs.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { AppContext } from 'pages/_app'; -import React, { useContext } from 'react'; -import { downloadAsFile } from 'utils/file'; -import constants from 'utils/strings/constants'; -import { addLogLine, getDebugLogs } from 'utils/logging'; -import SidebarButton from './Button'; -import { getData, LS_KEYS } from 'utils/storage/localStorage'; -import { User } from 'types/user'; -import { getSentryUserID } from 'utils/user'; - -export default function DebugLogs() { - const appContext = useContext(AppContext); - const confirmLogDownload = () => - appContext.setDialogMessage({ - title: constants.DOWNLOAD_LOGS, - content: constants.DOWNLOAD_LOGS_MESSAGE(), - proceed: { - text: constants.DOWNLOAD, - variant: 'accent', - action: downloadDebugLogs, - }, - close: { - text: constants.CANCEL, - }, - }); - - const downloadDebugLogs = () => { - addLogLine( - 'latest commit id :' + process.env.NEXT_PUBLIC_LATEST_COMMIT_HASH - ); - addLogLine(`user sentry id ${getSentryUserID()}`); - addLogLine(`ente userID ${(getData(LS_KEYS.USER) as User)?.id}`); - addLogLine('exporting logs'); - const logs = getDebugLogs(); - const logString = logs.join('\n'); - downloadAsFile(`debug_logs_${Date.now()}.txt`, logString); - }; - - return ( - - {constants.DOWNLOAD_UPLOAD_LOGS} - - ); -} diff --git a/src/components/Sidebar/DebugSection.tsx b/src/components/Sidebar/DebugSection.tsx new file mode 100644 index 000000000..fb8910ffd --- /dev/null +++ b/src/components/Sidebar/DebugSection.tsx @@ -0,0 +1,84 @@ +import { AppContext } from 'pages/_app'; +import React, { useContext, useEffect, useState } from 'react'; +import { downloadAsFile } from 'utils/file'; +import constants from 'utils/strings/constants'; +import { addLogLine, getDebugLogs } from 'utils/logging'; +import SidebarButton from './Button'; +import isElectron from 'is-electron'; +import ElectronService from 'services/electron/common'; +import Typography from '@mui/material/Typography'; +import { isInternalUser } from 'utils/user'; +import { testUpload } from '../../../tests/upload.test'; +import { + testZipFileReading, + testZipWithRootFileReadingTest, +} from '../../../tests/zip-file-reading.test'; + +export default function DebugSection() { + const appContext = useContext(AppContext); + const [appVersion, setAppVersion] = useState(null); + + useEffect(() => { + const main = async () => { + if (isElectron()) { + const appVersion = await ElectronService.getAppVersion(); + setAppVersion(appVersion); + } + }; + main(); + }); + + const confirmLogDownload = () => + appContext.setDialogMessage({ + title: constants.DOWNLOAD_LOGS, + content: constants.DOWNLOAD_LOGS_MESSAGE(), + proceed: { + text: constants.DOWNLOAD, + variant: 'accent', + action: downloadDebugLogs, + }, + close: { + text: constants.CANCEL, + }, + }); + + const downloadDebugLogs = () => { + addLogLine('exporting logs'); + if (isElectron()) { + ElectronService.openLogDirectory(); + } else { + const logs = getDebugLogs(); + + downloadAsFile(`debug_logs_${Date.now()}.txt`, logs); + } + }; + + return ( + <> + + {constants.DOWNLOAD_UPLOAD_LOGS} + + {appVersion && ( + + {appVersion} + + )} + {isInternalUser() && ( + <> + + Test Upload + + + Test Zip file reading + + + Zip with Root file Test + + + )} + + ); +} diff --git a/src/components/Sidebar/HelpSection.tsx b/src/components/Sidebar/HelpSection.tsx index 6e421c732..ac193b0bb 100644 --- a/src/components/Sidebar/HelpSection.tsx +++ b/src/components/Sidebar/HelpSection.tsx @@ -10,6 +10,7 @@ import { AppContext } from 'pages/_app'; import EnteSpinner from 'components/EnteSpinner'; import { getDownloadAppMessage } from 'utils/ui'; import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte'; +import { openLink } from 'utils/common'; export default function HelpSection() { const [exportModalView, setExportModalView] = useState(false); @@ -20,8 +21,7 @@ export default function HelpSection() { const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent( getToken() )}`; - const win = window.open(feedbackURL, '_blank'); - win.focus(); + openLink(feedbackURL, true); } function exportFiles() { diff --git a/src/components/Sidebar/ShortcutSection.tsx b/src/components/Sidebar/ShortcutSection.tsx index bb0a85cd1..c922453ab 100644 --- a/src/components/Sidebar/ShortcutSection.tsx +++ b/src/components/Sidebar/ShortcutSection.tsx @@ -2,10 +2,10 @@ import React, { useContext } from 'react'; import constants from 'utils/strings/constants'; import { GalleryContext } from 'pages/gallery'; import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection'; -import DeleteIcon from '@mui/icons-material/Delete'; -import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import { CollectionSummaries } from 'types/collection'; import ShortcutButton from './ShortcutButton'; +import DeleteOutline from '@mui/icons-material/DeleteOutline'; +import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined'; interface Iprops { closeSidebar: () => void; collectionSummaries: CollectionSummaries; @@ -30,13 +30,13 @@ export default function ShortcutSection({ return ( <> } + startIcon={} label={constants.TRASH} count={collectionSummaries.get(TRASH_SECTION)?.fileCount} onClick={openTrashSection} /> } + startIcon={} label={constants.ARCHIVE_SECTION_NAME} count={collectionSummaries.get(ARCHIVE_SECTION)?.fileCount} onClick={openArchiveSection} diff --git a/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx b/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx index 3dfbd5e50..a8e0691ab 100644 --- a/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx +++ b/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx @@ -16,7 +16,7 @@ export function FamilyUsageProgressBar({ ); diff --git a/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx b/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx index 22ef34b5c..c03c5ce19 100644 --- a/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx +++ b/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx @@ -13,7 +13,7 @@ interface Iprops { export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) { return ( - + { - if (userDetails) { - if (isSubscriptionActive(userDetails.subscription)) { - if (hasExceededStorageQuota(userDetails)) { - return showPlanSelectorModal; - } - } else { - if (hasStripeSubscription(userDetails.subscription)) { - return billingService.redirectToCustomerPortal; + const eventHandler: MouseEventHandler = (e) => { + e.stopPropagation(); + if (userDetails) { + if (isSubscriptionActive(userDetails.subscription)) { + if (hasExceededStorageQuota(userDetails)) { + showPlanSelectorModal(); + } } else { - return showPlanSelectorModal; + if (hasStripeSubscription(userDetails.subscription)) { + billingService.redirectToCustomerPortal(); + } else { + showPlanSelectorModal(); + } } } - } + }; + return eventHandler; }, [userDetails]); if (!hasAMessage) { @@ -80,13 +84,9 @@ export default function SubscriptionStatus({ ) : hasExceededStorageQuota(userDetails) && constants.STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO( - showPlanSelectorModal + handleClick ) - : constants.SUBSCRIPTION_EXPIRED_MESSAGE( - hasStripeSubscription(userDetails.subscription) - ? billingService.redirectToCustomerPortal - : showPlanSelectorModal - )} + : constants.SUBSCRIPTION_EXPIRED_MESSAGE(handleClick)} ); diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 5dc8c5110..f58b41557 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -4,7 +4,7 @@ import ShortcutSection from './ShortcutSection'; import UtilitySection from './UtilitySection'; import HelpSection from './HelpSection'; import ExitSection from './ExitSection'; -import DebugLogs from './DebugLogs'; +import DebugSection from './DebugSection'; import { DrawerSidebar } from './styledComponents'; import HeaderSection from './Header'; import { CollectionSummaries } from 'types/collection'; @@ -37,7 +37,7 @@ export default function Sidebar({ - + ); diff --git a/src/components/Sidebar/styledComponents.tsx b/src/components/Sidebar/styledComponents.tsx index 433c9ab0a..15c11f83f 100644 --- a/src/components/Sidebar/styledComponents.tsx +++ b/src/components/Sidebar/styledComponents.tsx @@ -1,11 +1,9 @@ -import { Drawer, styled } from '@mui/material'; +import { styled } from '@mui/material'; import CircleIcon from '@mui/icons-material/Circle'; +import { EnteDrawer } from 'components/EnteDrawer'; -export const DrawerSidebar = styled(Drawer)(({ theme }) => ({ +export const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({ '& .MuiPaper-root': { - maxWidth: '375px', - width: '100%', - scrollbarWidth: 'thin', padding: theme.spacing(1.5), }, })); diff --git a/src/components/SignUp.tsx b/src/components/SignUp.tsx index 8e8c25d68..e4c68985b 100644 --- a/src/components/SignUp.tsx +++ b/src/components/SignUp.tsx @@ -47,8 +47,12 @@ export default function SignUp(props: SignUpProps) { { email, passphrase, confirm }: FormValues, { setFieldError }: FormikHelpers ) => { - setLoading(true); try { + if (passphrase !== confirm) { + setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR); + return; + } + setLoading(true); try { setData(LS_KEYS.USER, { email }); await sendOtt(email); @@ -60,30 +64,23 @@ export default function SignUp(props: SignUpProps) { throw e; } try { - if (passphrase === confirm) { - const { keyAttributes, masterKey } = - await generateKeyAttributes(passphrase); - setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes); - await generateAndSaveIntermediateKeyAttributes( - passphrase, - keyAttributes, - masterKey - ); - - await saveKeyInSessionStore( - SESSION_KEYS.ENCRYPTION_KEY, - masterKey - ); - setJustSignedUp(true); - router.push(PAGES.VERIFY); - } else { - setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR); - } - } catch (e) { - setFieldError( - 'passphrase', - constants.PASSWORD_GENERATION_FAILED + const { keyAttributes, masterKey } = + await generateKeyAttributes(passphrase); + setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes); + await generateAndSaveIntermediateKeyAttributes( + passphrase, + keyAttributes, + masterKey ); + + await saveKeyInSessionStore( + SESSION_KEYS.ENCRYPTION_KEY, + masterKey + ); + setJustSignedUp(true); + router.push(PAGES.VERIFY); + } catch (e) { + setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED); throw e; } } catch (err) { diff --git a/src/components/SingleInputForm.tsx b/src/components/SingleInputForm.tsx index 81b9be9c9..e1a0ebb5d 100644 --- a/src/components/SingleInputForm.tsx +++ b/src/components/SingleInputForm.tsx @@ -6,7 +6,7 @@ import SubmitButton from './SubmitButton'; import TextField from '@mui/material/TextField'; import ShowHidePassword from './Form/ShowHidePassword'; import { FlexWrapper } from './Container'; -import { Button } from '@mui/material'; +import { Button, FormHelperText } from '@mui/material'; interface formValues { inputValue: string; @@ -24,8 +24,11 @@ export interface SingleInputFormProps { secondaryButtonAction?: () => void; disableAutoFocus?: boolean; hiddenPreInput?: any; + caption?: any; hiddenPostInput?: any; autoComplete?: string; + blockButton?: boolean; + hiddenLabel?: boolean; } export default function SingleInputForm(props: SingleInputFormProps) { @@ -86,12 +89,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
{props.hiddenPreInput} + + {props.caption} + {props.hiddenPostInput} - + {props.secondaryButtonAction && ( )} > = ({ disabled={disabled || loading || success} sx={{ my: 4, - '&.Mui-disabled': { - backgroundColor: (theme) => theme.palette.accent.main, - color: (theme) => theme.palette.text.primary, - }, + ...(loading + ? { + '&.Mui-disabled': { + backgroundColor: (theme) => + theme.palette.accent.main, + color: (theme) => theme.palette.text.primary, + }, + } + : {}), ...sx, }} {...props}> diff --git a/src/components/Titlebar.tsx b/src/components/Titlebar.tsx new file mode 100644 index 000000000..cb8f9f00f --- /dev/null +++ b/src/components/Titlebar.tsx @@ -0,0 +1,57 @@ +import Close from '@mui/icons-material/Close'; +import ArrowBack from '@mui/icons-material/ArrowBack'; +import { Box, IconButton, Typography } from '@mui/material'; +import React from 'react'; +import { FlexWrapper } from './Container'; + +interface Iprops { + title: string; + caption?: string; + onClose: () => void; + backIsClose?: boolean; + onRootClose?: () => void; + actionButton?: JSX.Element; +} + +export default function Titlebar({ + title, + caption, + onClose, + backIsClose, + actionButton, + onRootClose, +}: Iprops): JSX.Element { + return ( + <> + + + {backIsClose ? : } + + + {actionButton && actionButton} + {!backIsClose && ( + + + + )} + + + + + {title} + + + {caption} + + + + ); +} diff --git a/src/components/Upload/UploadButton.tsx b/src/components/Upload/UploadButton.tsx index 916b99e05..dc61e623d 100644 --- a/src/components/Upload/UploadButton.tsx +++ b/src/components/Upload/UploadButton.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { IconButton, styled } from '@mui/material'; +import { ButtonProps, IconButton, styled } from '@mui/material'; import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined'; import { Button } from '@mui/material'; import constants from 'utils/strings/constants'; import uploadManager from 'services/upload/uploadManager'; -const Wrapper = styled('div')` +const Wrapper = styled('div')<{ $disableShrink: boolean }>` display: flex; align-items: center; justify-content: center; @@ -14,22 +14,35 @@ const Wrapper = styled('div')` & .mobile-button { display: none; } - @media (max-width: 624px) { + ${({ $disableShrink }) => + !$disableShrink && + `@media (max-width: 624px) { & .mobile-button { display: block; } & .desktop-button { display: none; } - } + }`} `; interface Iprops { openUploader: () => void; + text?: string; + color?: ButtonProps['color']; + disableShrink?: boolean; + icon?: JSX.Element; } -function UploadButton({ openUploader }: Iprops) { +function UploadButton({ + openUploader, + text, + color, + disableShrink, + icon, +}: Iprops) { return ( @@ -37,9 +50,9 @@ function UploadButton({ openUploader }: Iprops) { onClick={openUploader} disabled={!uploadManager.shouldAllowNewUpload()} className="desktop-button" - color="secondary" - startIcon={}> - {constants.UPLOAD} + color={color ?? 'secondary'} + startIcon={icon ?? }> + {text ?? constants.UPLOAD} 0 || finishedUploads.get(UPLOAD_RESULT.TOO_LARGE)?.length > 0 || - finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0 + finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0 || + finishedUploads.get(UPLOAD_RESULT.SKIPPED_VIDEOS)?.length > 0 ) { setHasUnUploadedFiles(true); } else { @@ -40,70 +42,85 @@ export function UploadProgressDialog() { {(uploadStage === UPLOAD_STAGES.UPLOADING || - uploadStage === UPLOAD_STAGES.FINISH) && ( + uploadStage === UPLOAD_STAGES.FINISH || + uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && ( - {uploadStage === UPLOAD_STAGES.UPLOADING && ( + {(uploadStage === UPLOAD_STAGES.UPLOADING || + uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && ( )} + {(uploadStage === UPLOAD_STAGES.UPLOADING || + uploadStage === UPLOAD_STAGES.FINISH) && ( + <> + + - - + {uploadStage === UPLOAD_STAGES.FINISH && + hasUnUploadedFiles && ( + + {constants.FILE_NOT_UPLOADED_LIST} + + )} - {uploadStage === UPLOAD_STAGES.FINISH && - hasUnUploadedFiles && ( - - {constants.FILE_NOT_UPLOADED_LIST} - - )} - - - - - - - + + + + + + + + + )} )} {uploadStage === UPLOAD_STAGES.FINISH && } diff --git a/src/components/Upload/UploadProgress/inProgressSection.tsx b/src/components/Upload/UploadProgress/inProgressSection.tsx index 418c2ed69..5266ce738 100644 --- a/src/components/Upload/UploadProgress/inProgressSection.tsx +++ b/src/components/Upload/UploadProgress/inProgressSection.tsx @@ -10,17 +10,19 @@ import { } from './section'; import UploadProgressContext from 'contexts/uploadProgress'; import constants from 'utils/strings/constants'; +import { UPLOAD_STAGES } from 'constants/upload'; export const InProgressSection = () => { - const { inProgressUploads, hasLivePhotos, uploadFileNames } = useContext( - UploadProgressContext - ); + const { inProgressUploads, hasLivePhotos, uploadFileNames, uploadStage } = + useContext(UploadProgressContext); const fileList = inProgressUploads ?? []; return ( - + }> - {constants.INPROGRESS_UPLOADS} + {uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA + ? constants.INPROGRESS_METADATA_EXTRACTION + : constants.INPROGRESS_UPLOADS} {hasLivePhotos && ( @@ -30,8 +32,13 @@ export const InProgressSection = () => { fileList={fileList.map(({ localFileID, progress }) => ( {uploadFileNames.get(localFileID)} - {`-`} - {`${progress}%`} + {uploadStage === UPLOAD_STAGES.UPLOADING && ( + <> + {' '} + {`-`} + {`${progress}%`} + + )} ))} /> diff --git a/src/components/Upload/UploadProgress/title.tsx b/src/components/Upload/UploadProgress/title.tsx index a900ab171..2bdcd2184 100644 --- a/src/components/Upload/UploadProgress/title.tsx +++ b/src/components/Upload/UploadProgress/title.tsx @@ -20,6 +20,8 @@ function UploadProgressSubtitleText() { return ( {uploadStage === UPLOAD_STAGES.UPLOADING + ? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter) + : uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA ? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter) : constants.UPLOAD_STAGE_MESSAGE[uploadStage]} diff --git a/src/components/Upload/UploadTypeSelector/index.tsx b/src/components/Upload/UploadTypeSelector/index.tsx index 6579e8df7..bbb6a32b2 100644 --- a/src/components/Upload/UploadTypeSelector/index.tsx +++ b/src/components/Upload/UploadTypeSelector/index.tsx @@ -1,19 +1,48 @@ -import React from 'react'; +import React, { useContext, useEffect, useRef } from 'react'; import constants from 'utils/strings/constants'; import { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined'; import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined'; import GoogleIcon from '@mui/icons-material/Google'; import { UploadTypeOption } from './option'; -import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton'; +import DialogTitleWithCloseButton, { + dialogCloseHandler, +} from 'components/DialogBox/TitleWithCloseButton'; import { Box, Dialog, Stack, Typography } from '@mui/material'; +import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; +import { isMobileOrTable } from 'utils/common/deviceDetection'; +interface Iprops { + onClose: () => void; + show: boolean; + uploadFiles: () => void; + uploadFolders: () => void; + uploadGoogleTakeoutZips: () => void; + hideZipUploadOption?: boolean; +} export default function UploadTypeSelector({ - onHide, + onClose, show, uploadFiles, uploadFolders, uploadGoogleTakeoutZips, -}) { + hideZipUploadOption, +}: Iprops) { + const publicCollectionGalleryContext = useContext( + PublicCollectionGalleryContext + ); + const directlyShowUploadFiles = useRef(isMobileOrTable()); + + useEffect(() => { + if ( + show && + directlyShowUploadFiles.current && + publicCollectionGalleryContext.accessedThroughSharedURL + ) { + uploadFiles(); + onClose(); + } + }, [show]); + return ( - - {constants.UPLOAD} + onClose={dialogCloseHandler({ onClose })}> + + {publicCollectionGalleryContext.accessedThroughSharedURL + ? constants.SELECT_PHOTOS + : constants.UPLOAD} @@ -40,11 +71,13 @@ export default function UploadTypeSelector({ startIcon={}> {constants.UPLOAD_DIRS} - }> - {constants.UPLOAD_GOOGLE_TAKEOUT} - + {!hideZipUploadOption && ( + }> + {constants.UPLOAD_GOOGLE_TAKEOUT} + + )} {constants.DRAG_AND_DROP_HINT} diff --git a/src/components/Upload/Uploader.tsx b/src/components/Upload/Uploader.tsx index b3190ec89..179543b29 100644 --- a/src/components/Upload/Uploader.tsx +++ b/src/components/Upload/Uploader.tsx @@ -10,7 +10,6 @@ import { SetCollections, SetCollectionSelectorAttributes } from 'types/gallery'; import { GalleryContext } from 'pages/gallery'; import { AppContext } from 'pages/_app'; import { logError } from 'utils/sentry'; -import UploadManager from 'services/upload/uploadManager'; import uploadManager from 'services/upload/uploadManager'; import ImportService from 'services/importService'; import isElectron from 'is-electron'; @@ -40,7 +39,10 @@ import { PICKED_UPLOAD_TYPE, } from 'constants/upload'; import importService from 'services/importService'; -import { getDownloadAppMessage } from 'utils/ui'; +import { + getDownloadAppMessage, + getRootLevelFileWithFolderNotAllowMessage, +} from 'utils/ui'; import UploadTypeSelector from './UploadTypeSelector'; import { filterOutSystemFiles, @@ -49,21 +51,29 @@ import { } from 'utils/upload'; import { getUserOwnedCollections } from 'utils/collection'; import billingService from 'services/billingService'; +import { addLogLine } from 'utils/logging'; +import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; +import UserNameInputDialog from 'components/UserNameInputDialog'; +import { + getPublicCollectionUID, + getPublicCollectionUploaderName, + savePublicCollectionUploaderName, +} from 'services/publicCollectionService'; const FIRST_ALBUM_NAME = 'My First Album'; interface Props { syncWithRemote: (force?: boolean, silent?: boolean) => Promise; - closeCollectionSelector: () => void; + closeCollectionSelector?: () => void; closeUploadTypeSelector: () => void; - setCollectionSelectorAttributes: SetCollectionSelectorAttributes; - setCollectionNamerAttributes: SetCollectionNamerAttributes; + setCollectionSelectorAttributes?: SetCollectionSelectorAttributes; + setCollectionNamerAttributes?: SetCollectionNamerAttributes; setLoading: SetLoading; setShouldDisableDropzone: (value: boolean) => void; - showCollectionSelector: () => void; + showCollectionSelector?: () => void; setFiles: SetFiles; - setCollections: SetCollections; - isFirstUpload: boolean; + setCollections?: SetCollections; + isFirstUpload?: boolean; uploadTypeSelectorView: boolean; showSessionExpiredMessage: () => void; showUploadFilesDialog: () => void; @@ -71,9 +81,17 @@ interface Props { webFolderSelectorFiles: File[]; webFileSelectorFiles: File[]; dragAndDropFiles: File[]; + zipUploadDisabled?: boolean; + uploadCollection?: Collection; } export default function Uploader(props: Props) { + const appContext = useContext(AppContext); + const galleryContext = useContext(GalleryContext); + const publicCollectionGalleryContext = useContext( + PublicCollectionGalleryContext + ); + const [uploadProgressView, setUploadProgressView] = useState(false); const [uploadStage, setUploadStage] = useState( UPLOAD_STAGES.START @@ -92,11 +110,13 @@ export default function Uploader(props: Props) { const [hasLivePhotos, setHasLivePhotos] = useState(false); const [choiceModalView, setChoiceModalView] = useState(false); + const [userNameInputDialogView, setUserNameInputDialogView] = + useState(false); const [importSuggestion, setImportSuggestion] = useState( DEFAULT_IMPORT_SUGGESTION ); - const appContext = useContext(AppContext); - const galleryContext = useContext(GalleryContext); + const [electronFiles, setElectronFiles] = useState(null); + const [webFiles, setWebFiles] = useState([]); const toUploadFiles = useRef(null); const isPendingDesktopUpload = useRef(false); @@ -105,20 +125,33 @@ export default function Uploader(props: Props) { const pickedUploadType = useRef(null); const zipPaths = useRef(null); const currentUploadPromise = useRef>(null); - const [electronFiles, setElectronFiles] = useState(null); - const [webFiles, setWebFiles] = useState([]); + const uploadRunning = useRef(false); + const uploaderNameRef = useRef(null); const closeUploadProgress = () => setUploadProgressView(false); + const showUserNameInputDialog = () => setUserNameInputDialogView(true); const setCollectionName = (collectionName: string) => { isPendingDesktopUpload.current = true; pendingDesktopUploadCollectionName.current = collectionName; }; - const uploadRunning = useRef(false); + const handleChoiceModalClose = () => { + setChoiceModalView(false); + uploadRunning.current = false; + }; + const handleCollectionSelectorCancel = () => { + uploadRunning.current = false; + appContext.resetSharedFiles(); + }; + + const handleUserNameInputDialogClose = () => { + setUserNameInputDialogView(false); + uploadRunning.current = false; + }; useEffect(() => { - UploadManager.init( + uploadManager.init( { setPercentComplete, setUploadCounter, @@ -128,12 +161,16 @@ export default function Uploader(props: Props) { setUploadFilenames: setUploadFileNames, setHasLivePhotos, }, - props.setFiles + props.setFiles, + publicCollectionGalleryContext ); if (isElectron() && ImportService.checkAllElectronAPIsExists()) { ImportService.getPendingUploads().then( ({ files: electronFiles, collectionName, type }) => { + addLogLine( + `found pending desktop upload, resuming uploads` + ); resumeDesktopUpload(type, electronFiles, collectionName); } ); @@ -144,7 +181,11 @@ export default function Uploader(props: Props) { appContext.setIsFolderSyncRunning ); } - }, []); + }, [ + publicCollectionGalleryContext.accessedThroughSharedURL, + publicCollectionGalleryContext.token, + publicCollectionGalleryContext.passwordToken, + ]); // this handles the change of selectorFiles changes on web when user selects // files for upload through the opened file/folder selector or dragAndDrop them @@ -159,13 +200,16 @@ export default function Uploader(props: Props) { pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS && props.webFolderSelectorFiles?.length > 0 ) { + addLogLine(`received folder upload request`); setWebFiles(props.webFolderSelectorFiles); } else if ( pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES && props.webFileSelectorFiles?.length > 0 ) { + addLogLine(`received file upload request`); setWebFiles(props.webFileSelectorFiles); } else if (props.dragAndDropFiles?.length > 0) { + addLogLine(`received drag and drop upload request`); setWebFiles(props.dragAndDropFiles); } }, [ @@ -180,17 +224,37 @@ export default function Uploader(props: Props) { webFiles?.length > 0 || appContext.sharedFiles?.length > 0 ) { + addLogLine( + `upload request type:${ + electronFiles?.length > 0 + ? 'electronFiles' + : webFiles?.length > 0 + ? 'webFiles' + : 'sharedFiles' + } count ${ + electronFiles?.length ?? + webFiles?.length ?? + appContext?.sharedFiles.length + }` + ); if (uploadRunning.current) { if (watchFolderService.isUploadRunning()) { + addLogLine( + 'watchFolder upload was running, pausing it to run user upload' + ); // pause watch folder service on user upload watchFolderService.pauseRunningSync(); } else { + addLogLine( + 'an upload is already running, rejecting new upload request' + ); // no-op // a user upload is already in progress return; } } if (isCanvasBlocked()) { + addLogLine('canvas blocked, blocking upload'); appContext.setDialogMessage({ title: constants.CANVAS_BLOCKED_TITLE, @@ -235,7 +299,8 @@ export default function Uploader(props: Props) { handleCollectionCreationAndUpload( importSuggestion, props.isFirstUpload, - pickedUploadType.current + pickedUploadType.current, + publicCollectionGalleryContext.accessedThroughSharedURL ); pickedUploadType.current = null; props.setLoading(false); @@ -256,14 +321,20 @@ export default function Uploader(props: Props) { }; const preCollectionCreationAction = async () => { - props.closeCollectionSelector(); + props.closeCollectionSelector?.(); props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload()); setUploadStage(UPLOAD_STAGES.START); setUploadProgressView(true); }; - const uploadFilesToExistingCollection = async (collection: Collection) => { + const uploadFilesToExistingCollection = async ( + collection: Collection, + uploaderName?: string + ) => { try { + addLogLine( + `upload file to an existing collection - "${collection.name}"` + ); await preCollectionCreationAction(); const filesWithCollectionToUpload: FileWithCollection[] = toUploadFiles.current.map((file, index) => ({ @@ -271,10 +342,11 @@ export default function Uploader(props: Props) { localID: index, collectionID: collection.id, })); - waitInQueueAndUploadFiles(filesWithCollectionToUpload, [ - collection, - ]); - toUploadFiles.current = null; + waitInQueueAndUploadFiles( + filesWithCollectionToUpload, + [collection], + uploaderName + ); } catch (e) { logError(e, 'Failed to upload files to existing collections'); } @@ -285,8 +357,11 @@ export default function Uploader(props: Props) { collectionName?: string ) => { try { + addLogLine( + `upload file to an new collections strategy:${strategy} ,collectionName:${collectionName}` + ); await preCollectionCreationAction(); - const filesWithCollectionToUpload: FileWithCollection[] = []; + let filesWithCollectionToUpload: FileWithCollection[] = []; const collections: Collection[] = []; let collectionNameToFilesMap = new Map< string, @@ -302,6 +377,9 @@ export default function Uploader(props: Props) { toUploadFiles.current ); } + addLogLine( + `upload collections - [${[...collectionNameToFilesMap.keys()]}]` + ); try { const existingCollection = getUserOwnedCollections( await syncCollections() @@ -320,13 +398,14 @@ export default function Uploader(props: Props) { ...existingCollection, ...collections, ]); - filesWithCollectionToUpload.push( + filesWithCollectionToUpload = [ + ...filesWithCollectionToUpload, ...files.map((file) => ({ localID: index++, collectionID: collection.id, file, - })) - ); + })), + ]; } } catch (e) { closeUploadProgress(); @@ -348,13 +427,18 @@ export default function Uploader(props: Props) { const waitInQueueAndUploadFiles = ( filesWithCollectionToUploadIn: FileWithCollection[], - collections: Collection[] + collections: Collection[], + uploaderName?: string ) => { const currentPromise = currentUploadPromise.current; currentUploadPromise.current = waitAndRun( currentPromise, async () => - await uploadFiles(filesWithCollectionToUploadIn, collections) + await uploadFiles( + filesWithCollectionToUploadIn, + collections, + uploaderName + ) ); }; @@ -372,9 +456,11 @@ export default function Uploader(props: Props) { const uploadFiles = async ( filesWithCollectionToUploadIn: FileWithCollection[], - collections: Collection[] + collections: Collection[], + uploaderName?: string ) => { try { + addLogLine('uploadFiles called'); preUploadAction(); if ( isElectron() && @@ -399,7 +485,8 @@ export default function Uploader(props: Props) { const shouldCloseUploadProgress = await uploadManager.queueFilesForUpload( filesWithCollectionToUploadIn, - collections + collections, + uploaderName ); if (shouldCloseUploadProgress) { closeUploadProgress(); @@ -416,6 +503,7 @@ export default function Uploader(props: Props) { } } } catch (err) { + logError(err, 'failed to upload files'); showUserFacingError(err.message); closeUploadProgress(); throw err; @@ -426,14 +514,18 @@ export default function Uploader(props: Props) { const retryFailed = async () => { try { + addLogLine('user retrying failed upload'); const filesWithCollections = - await uploadManager.getFailedFilesWithCollections(); + uploadManager.getFailedFilesWithCollections(); + const uploaderName = uploadManager.getUploaderName(); await preUploadAction(); await uploadManager.queueFilesForUpload( filesWithCollections.files, - filesWithCollections.collections + filesWithCollections.collections, + uploaderName ); } catch (err) { + logError(err, 'retry failed files failed'); showUserFacingError(err.message); closeUploadProgress(); } finally { @@ -449,35 +541,33 @@ export default function Uploader(props: Props) { case CustomError.SUBSCRIPTION_EXPIRED: notification = { variant: 'danger', - message: constants.SUBSCRIPTION_EXPIRED, - action: { - text: constants.RENEW_NOW, - callback: billingService.redirectToCustomerPortal, - }, + subtext: constants.SUBSCRIPTION_EXPIRED, + message: constants.RENEW_NOW, + onClick: () => billingService.redirectToCustomerPortal(), }; break; case CustomError.STORAGE_QUOTA_EXCEEDED: notification = { variant: 'danger', - message: constants.STORAGE_QUOTA_EXCEEDED, - action: { - text: constants.UPGRADE_NOW, - callback: galleryContext.showPlanSelectorModal, - }, - icon: , + subtext: constants.STORAGE_QUOTA_EXCEEDED, + message: constants.UPGRADE_NOW, + onClick: () => galleryContext.showPlanSelectorModal(), + startIcon: , }; break; default: notification = { variant: 'danger', message: constants.UNKNOWN_ERROR, + onClick: () => null, }; } - galleryContext.setNotificationAttributes(notification); + appContext.setNotificationAttributes(notification); } const uploadToSingleNewCollection = (collectionName: string) => { if (collectionName) { + addLogLine(`upload to single collection - "${collectionName}"`); uploadFilesToNewCollections( UPLOAD_STRATEGY.SINGLE_COLLECTION, collectionName @@ -495,45 +585,75 @@ export default function Uploader(props: Props) { }); }; - const handleCollectionCreationAndUpload = ( + const handleCollectionCreationAndUpload = async ( importSuggestion: ImportSuggestion, isFirstUpload: boolean, - pickedUploadType: PICKED_UPLOAD_TYPE + pickedUploadType: PICKED_UPLOAD_TYPE, + accessedThroughSharedURL?: boolean ) => { - if (isPendingDesktopUpload.current) { - isPendingDesktopUpload.current = false; - if (pendingDesktopUploadCollectionName.current) { - uploadToSingleNewCollection( - pendingDesktopUploadCollectionName.current + try { + if (accessedThroughSharedURL) { + addLogLine( + `uploading files to pulbic collection - ${props.uploadCollection.name} - ${props.uploadCollection.id}` ); - pendingDesktopUploadCollectionName.current = null; - } else { + const uploaderName = await getPublicCollectionUploaderName( + getPublicCollectionUID(publicCollectionGalleryContext.token) + ); + uploaderNameRef.current = uploaderName; + showUserNameInputDialog(); + return; + } + if (isPendingDesktopUpload.current) { + isPendingDesktopUpload.current = false; + if (pendingDesktopUploadCollectionName.current) { + addLogLine( + `upload pending files to collection - ${pendingDesktopUploadCollectionName.current}` + ); + uploadToSingleNewCollection( + pendingDesktopUploadCollectionName.current + ); + pendingDesktopUploadCollectionName.current = null; + } else { + addLogLine( + `pending upload - strategy - "multiple collections" ` + ); + uploadFilesToNewCollections( + UPLOAD_STRATEGY.COLLECTION_PER_FOLDER + ); + } + return; + } + if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) { + addLogLine('uploading zip files'); uploadFilesToNewCollections( UPLOAD_STRATEGY.COLLECTION_PER_FOLDER ); + return; } - return; + if (isFirstUpload && !importSuggestion.rootFolderName) { + importSuggestion.rootFolderName = FIRST_ALBUM_NAME; + } + let showNextModal = () => {}; + if (importSuggestion.hasNestedFolders) { + addLogLine(`nested folders detected`); + showNextModal = () => setChoiceModalView(true); + } else { + showNextModal = () => + uploadToSingleNewCollection( + importSuggestion.rootFolderName + ); + } + props.setCollectionSelectorAttributes({ + callback: uploadFilesToExistingCollection, + onCancel: handleCollectionSelectorCancel, + showNextModal, + title: constants.UPLOAD_TO_COLLECTION, + }); + } catch (e) { + logError(e, 'handleCollectionCreationAndUpload failed'); } - if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) { - uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER); - return; - } - if (isFirstUpload && !importSuggestion.rootFolderName) { - importSuggestion.rootFolderName = FIRST_ALBUM_NAME; - } - let showNextModal = () => {}; - if (importSuggestion.hasNestedFolders) { - showNextModal = () => setChoiceModalView(true); - } else { - showNextModal = () => - uploadToSingleNewCollection(importSuggestion.rootFolderName); - } - props.setCollectionSelectorAttributes({ - callback: uploadFilesToExistingCollection, - showNextModal, - title: constants.UPLOAD_TO_COLLECTION, - }); }; + const handleDesktopUpload = async (type: PICKED_UPLOAD_TYPE) => { let files: ElectronFile[]; pickedUploadType.current = type; @@ -547,6 +667,9 @@ export default function Uploader(props: Props) { zipPaths.current = response.zipPaths; } if (files?.length > 0) { + addLogLine( + ` desktop upload for type:${type} and fileCount: ${files?.length} requested` + ); setElectronFiles(files); props.closeUploadTypeSelector(); } @@ -579,26 +702,57 @@ export default function Uploader(props: Props) { const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS); const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS); + const handlePublicUpload = async ( + uploaderName: string, + skipSave?: boolean + ) => { + try { + if (!skipSave) { + savePublicCollectionUploaderName( + getPublicCollectionUID( + publicCollectionGalleryContext.token + ), + uploaderName + ); + } + await uploadFilesToExistingCollection( + props.uploadCollection, + uploaderName + ); + } catch (e) { + logError(e, 'public upload failed '); + } + }; + + const handleUploadToSingleCollection = () => { + uploadToSingleNewCollection(importSuggestion.rootFolderName); + }; + + const handleUploadToMultipleCollections = () => { + if (importSuggestion.hasRootLevelFileWithFolder) { + appContext.setDialogMessage( + getRootLevelFileWithFolderNotAllowMessage() + ); + return; + } + uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER); + }; + return ( <> setChoiceModalView(false)} - uploadToSingleCollection={() => - uploadToSingleNewCollection(importSuggestion.rootFolderName) - } - uploadToMultipleCollection={() => - uploadFilesToNewCollections( - UPLOAD_STRATEGY.COLLECTION_PER_FOLDER - ) - } + onClose={handleChoiceModalClose} + uploadToSingleCollection={handleUploadToSingleCollection} + uploadToMultipleCollection={handleUploadToMultipleCollections} /> + ); } diff --git a/src/components/UserNameInputDialog.tsx b/src/components/UserNameInputDialog.tsx new file mode 100644 index 000000000..839ff0fe1 --- /dev/null +++ b/src/components/UserNameInputDialog.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import constants from 'utils/strings/constants'; +import DialogBox from './DialogBox'; +import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; +import { Typography } from '@mui/material'; +import SingleInputForm from './SingleInputForm'; + +export default function UserNameInputDialog({ + open, + onClose, + onNameSubmit, + toUploadFilesCount, + uploaderName, +}) { + const handleSubmit = async (inputValue: string) => { + onClose(); + await onNameSubmit(inputValue); + }; + return ( + , + }}> + + {constants.PUBLIC_UPLOADER_NAME_MESSAGE} + + + + ); +} diff --git a/src/components/VerifyMasterPasswordForm.tsx b/src/components/VerifyMasterPasswordForm.tsx index ec579b67b..74b8dd548 100644 --- a/src/components/VerifyMasterPasswordForm.tsx +++ b/src/components/VerifyMasterPasswordForm.tsx @@ -1,7 +1,6 @@ import React from 'react'; import constants from 'utils/strings/constants'; -import CryptoWorker from 'utils/crypto'; import SingleInputForm, { SingleInputFormProps, } from 'components/SingleInputForm'; @@ -10,6 +9,7 @@ import { CustomError } from 'utils/error'; import { Input } from '@mui/material'; import { KeyAttributes, User } from 'types/user'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; export interface VerifyMasterPasswordFormProps { user: User; @@ -29,7 +29,7 @@ export default function VerifyMasterPasswordForm({ setFieldError ) => { try { - const cryptoWorker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); let kek: string = null; try { kek = await cryptoWorker.deriveKey( @@ -43,7 +43,7 @@ export default function VerifyMasterPasswordForm({ throw Error(CustomError.WEAK_DEVICE); } try { - const key: string = await cryptoWorker.decryptB64( + const key = await cryptoWorker.decryptB64( keyAttributes.encryptedKey, keyAttributes.keyDecryptionNonce, kek diff --git a/src/components/WatchFolder/styledComponents.tsx b/src/components/WatchFolder/styledComponents.tsx index 3e6d595c1..2af051bbe 100644 --- a/src/components/WatchFolder/styledComponents.tsx +++ b/src/components/WatchFolder/styledComponents.tsx @@ -3,15 +3,12 @@ import { Box } from '@mui/material'; import { styled } from '@mui/material/styles'; import VerticallyCentered from 'components/Container'; -export const MappingsContainer = styled(Box)(({ theme }) => ({ +export const MappingsContainer = styled(Box)(() => ({ height: '278px', overflow: 'auto', '&::-webkit-scrollbar': { width: '4px', }, - '&::-webkit-scrollbar-thumb': { - backgroundColor: theme.palette.secondary.main, - }, })); export const NoMappingsContainer = styled(VerticallyCentered)({ diff --git a/src/components/icons/ente.tsx b/src/components/icons/ente.tsx new file mode 100644 index 000000000..9a8e0afbf --- /dev/null +++ b/src/components/icons/ente.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export default function Ente() { + return ( + + + + ); +} diff --git a/src/components/pages/dedupe/SelectedFileOptions.tsx b/src/components/pages/dedupe/SelectedFileOptions.tsx index 030077d4c..c6a4c80ac 100644 --- a/src/components/pages/dedupe/SelectedFileOptions.tsx +++ b/src/components/pages/dedupe/SelectedFileOptions.tsx @@ -2,9 +2,8 @@ import { FluidContainer } from 'components/Container'; import { SelectionBar } from '../../Navbar/SelectionBar'; import constants from 'utils/strings/constants'; import React, { useContext } from 'react'; -import { Box, IconButton, styled } from '@mui/material'; +import { Box, IconButton, styled, Tooltip } from '@mui/material'; import { DeduplicateContext } from 'pages/deduplicate'; -import { IconWithMessage } from 'components/IconWithMessage'; import { AppContext } from 'pages/_app'; import CloseIcon from '@mui/icons-material/Close'; import BackButton from '@mui/icons-material/ArrowBackOutlined'; @@ -78,11 +77,11 @@ export default function DeduplicateOptions({
- + - + ); } diff --git a/src/components/pages/gallery/LinkButton.tsx b/src/components/pages/gallery/LinkButton.tsx index a56d68214..4c26433e3 100644 --- a/src/components/pages/gallery/LinkButton.tsx +++ b/src/components/pages/gallery/LinkButton.tsx @@ -1,34 +1,12 @@ -import Link, { LinkProps } from '@mui/material/Link'; +import { ButtonProps, Link, LinkProps } from '@mui/material'; import React, { FC } from 'react'; -import { ButtonProps } from 'react-bootstrap'; -export enum ButtonVariant { - success = 'success', - danger = 'danger', - secondary = 'secondary', - warning = 'warning', -} export type LinkButtonProps = React.PropsWithChildren<{ onClick: () => void; variant?: string; style?: React.CSSProperties; }>; -export function getVariantColor(variant: string) { - switch (variant) { - case ButtonVariant.success: - return '#51cd7c'; - case ButtonVariant.danger: - return '#c93f3f'; - case ButtonVariant.secondary: - return '#858585'; - case ButtonVariant.warning: - return '#D7BB63'; - default: - return '#d1d1d1'; - } -} - const LinkButton: FC> = ({ children, sx, @@ -41,6 +19,7 @@ const LinkButton: FC> = ({ sx={{ color: 'text.primary', textDecoration: 'underline rgba(255, 255, 255, 0.4)', + paddingBottom: 0.5, '&:hover': { color: `${color}.main`, textDecoration: `underline `, diff --git a/src/components/pages/gallery/Navbar.tsx b/src/components/pages/gallery/Navbar.tsx index 9eae71fba..4e62e1b79 100644 --- a/src/components/pages/gallery/Navbar.tsx +++ b/src/components/pages/gallery/Navbar.tsx @@ -1,7 +1,6 @@ import React from 'react'; import NavbarBase from 'components/Navbar/base'; import SidebarToggler from 'components/Navbar/SidebarToggler'; -import { getNonTrashedUniqueUserFiles } from 'utils/file'; import SearchBar from 'components/Search/SearchBar'; import { Collection } from 'types/collection'; import { EnteFile } from 'types/file'; @@ -36,7 +35,7 @@ export function GalleryNavbar({ isInSearchMode={isInSearchMode} setIsInSearchMode={setIsInSearchMode} collections={collections} - files={getNonTrashedUniqueUserFiles(files)} + files={files} setActiveCollection={setActiveCollection} updateSearch={updateSearch} /> diff --git a/src/components/pages/gallery/PlanSelector/card/index.tsx b/src/components/pages/gallery/PlanSelector/card/index.tsx index 4af88bbed..2d8ed195a 100644 --- a/src/components/pages/gallery/PlanSelector/card/index.tsx +++ b/src/components/pages/gallery/PlanSelector/card/index.tsx @@ -13,6 +13,7 @@ import { hasPaidSubscription, getTotalFamilyUsage, isPartOfFamily, + isSubscriptionActive, } from 'utils/billing'; import { reverseString } from 'utils/common'; import { GalleryContext } from 'pages/gallery'; @@ -68,18 +69,19 @@ function PlanSelectorCard(props: Props) { const main = async () => { try { props.setLoading(true); - let plans = await billingService.getPlans(); - - const planNotListed = - plans.filter((plan) => - isUserSubscribedPlan(plan, subscription) - ).length === 0; - if ( - subscription && - !isOnFreePlan(subscription) && - planNotListed - ) { - plans = [planForSubscription(subscription), ...plans]; + const plans = await billingService.getPlans(); + if (isSubscriptionActive(subscription)) { + const planNotListed = + plans.filter((plan) => + isUserSubscribedPlan(plan, subscription) + ).length === 0; + if ( + subscription && + !isOnFreePlan(subscription) && + planNotListed + ) { + plans.push(planForSubscription(subscription)); + } } setPlans(plans); } catch (e) { diff --git a/src/components/pages/gallery/PlanSelector/manageSubscription/index.tsx b/src/components/pages/gallery/PlanSelector/manageSubscription/index.tsx index 5b9166e78..6baaddc40 100644 --- a/src/components/pages/gallery/PlanSelector/manageSubscription/index.tsx +++ b/src/components/pages/gallery/PlanSelector/manageSubscription/index.tsx @@ -2,6 +2,7 @@ import { Stack } from '@mui/material'; import { AppContext } from 'pages/_app'; import React, { useContext } from 'react'; import { Subscription } from 'types/billing'; +import { SetLoading } from 'types/gallery'; import { activateSubscription, cancelSubscription, @@ -15,7 +16,7 @@ import ManageSubscriptionButton from './button'; interface Iprops { subscription: Subscription; closeModal: () => void; - setLoading: (value: boolean) => void; + setLoading: SetLoading; } export function ManageSubscription({ diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx index aa42e9ed4..16a1d1b82 100644 --- a/src/components/pages/gallery/PreviewCard.tsx +++ b/src/components/pages/gallery/PreviewCard.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useLayoutEffect, useRef, useState } from 'react'; +import React, { useContext, useLayoutEffect, useState } from 'react'; import { EnteFile } from 'types/file'; import { styled } from '@mui/material'; import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined'; @@ -14,22 +14,22 @@ import { DeduplicateContext } from 'pages/deduplicate'; import { logError } from 'utils/sentry'; import { Overlay } from 'components/Container'; import { TRASH_SECTION } from 'constants/collection'; -import { formatDateRelative } from 'utils/time'; +import { formatDateRelative } from 'utils/time/format'; interface IProps { file: EnteFile; - updateURL?: (url: string) => EnteFile; - onClick?: () => void; - forcedEnable?: boolean; - selectable?: boolean; - selected?: boolean; - onSelect?: (checked: boolean) => void; - onHover?: () => void; - onRangeSelect?: () => void; - isRangeSelectActive?: boolean; - selectOnClick?: boolean; - isInsSelectRange?: boolean; - activeCollection?: number; + updateURL: (url: string) => EnteFile; + onClick: () => void; + selectable: boolean; + selected: boolean; + onSelect: (checked: boolean) => void; + onHover: () => void; + onRangeSelect: () => void; + isRangeSelectActive: boolean; + selectOnClick: boolean; + isInsSelectRange: boolean; + activeCollection: number; + showPlaceholder: boolean; } const Check = styled('input')<{ $active: boolean }>` @@ -203,7 +203,6 @@ export default function PreviewCard(props: IProps) { file, onClick, updateURL, - forcedEnable, selectable, selected, onSelect, @@ -213,14 +212,13 @@ export default function PreviewCard(props: IProps) { isRangeSelectActive, isInsSelectRange, } = props; - const isMounted = useRef(true); const publicCollectionGalleryContext = useContext( PublicCollectionGalleryContext ); const deduplicateContext = useContext(DeduplicateContext); useLayoutEffect(() => { - if (file && !file.msrc) { + if (file && !file.msrc && !props.showPlaceholder) { const main = async () => { try { let url; @@ -236,18 +234,14 @@ export default function PreviewCard(props: IProps) { } else { url = await DownloadManager.getThumbnail(file); } - if (isMounted.current) { - setImgSrc(url); - thumbs.set(file.id, url); - if (updateURL) { - const newFile = updateURL(url); - file.msrc = newFile.msrc; - file.html = newFile.html; - file.src = newFile.src; - file.w = newFile.w; - file.h = newFile.h; - } - } + setImgSrc(url); + thumbs.set(file.id, url); + const newFile = updateURL(url); + file.msrc = newFile.msrc; + file.html = newFile.html; + file.src = newFile.src; + file.w = newFile.w; + file.h = newFile.h; } catch (e) { logError(e, 'preview card useEffect failed'); // no-op @@ -257,17 +251,17 @@ export default function PreviewCard(props: IProps) { if (thumbs.has(file.id)) { const thumbImgSrc = thumbs.get(file.id); setImgSrc(thumbImgSrc); - file.msrc = thumbImgSrc; + const newFile = updateURL(thumbImgSrc); + file.msrc = newFile.msrc; + file.html = newFile.html; + file.src = newFile.src; + file.w = newFile.w; + file.h = newFile.h; } else { main(); } } - - return () => { - // cool cool cool - isMounted.current = false; - }; - }, [file]); + }, [file, props.showPlaceholder]); const handleClick = () => { if (selectOnClick) { @@ -300,10 +294,10 @@ export default function PreviewCard(props: IProps) { return ( {selectable && ( (OS.UNKNOWN); useEffect(() => { - const os = GetDeviceOS(); + const os = getDeviceOS(); setOS(os); }, []); diff --git a/src/components/pages/sharedAlbum/Navbar.tsx b/src/components/pages/sharedAlbum/Navbar.tsx index b3eebe395..537c612e5 100644 --- a/src/components/pages/sharedAlbum/Navbar.tsx +++ b/src/components/pages/sharedAlbum/Navbar.tsx @@ -1,18 +1,25 @@ -import { CenteredFlex, FluidContainer } from 'components/Container'; -import { EnteLogo } from 'components/EnteLogo'; +import { EnteLinkLogo } from 'components/Navbar/EnteLinkLogo'; +import { FluidContainer } from 'components/Container'; import NavbarBase from 'components/Navbar/base'; +import UploadButton from 'components/Upload/UploadButton'; import React from 'react'; +import constants from 'utils/strings/constants'; import GoToEnte from './GoToEnte'; -export default function SharedAlbumNavbar() { +export default function SharedAlbumNavbar({ showUploadButton, openUploader }) { return ( - - - + - + {showUploadButton ? ( + + ) : ( + + )} ); } diff --git a/src/constants/billing/index.ts b/src/constants/billing/index.ts index b445cbd96..66883392a 100644 --- a/src/constants/billing/index.ts +++ b/src/constants/billing/index.ts @@ -1 +1,4 @@ -export const DESKTOP_REDIRECT_URL = 'https://payments.ente.io/desktop-redirect'; +import { getPaymentsURL } from 'utils/common/apiUtil'; + +export const getDesktopRedirectURL = () => + `${getPaymentsURL()}/desktop-redirect`; diff --git a/src/constants/collection/index.ts b/src/constants/collection/index.ts index da4fe3d94..8677f749d 100644 --- a/src/constants/collection/index.ts +++ b/src/constants/collection/index.ts @@ -21,7 +21,6 @@ export enum CollectionSummaryType { export enum COLLECTION_SORT_BY { NAME, CREATION_TIME_ASCENDING, - CREATION_TIME_DESCENDING, UPDATION_TIME_DESCENDING, } @@ -56,7 +55,6 @@ export const UPLOAD_NOT_ALLOWED_COLLECTION_TYPES = new Set([ export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([ CollectionSummaryType.all, CollectionSummaryType.archive, - CollectionSummaryType.shared, CollectionSummaryType.favorites, ]); diff --git a/src/constants/ffmpeg/index.ts b/src/constants/ffmpeg/index.ts new file mode 100644 index 000000000..008b09784 --- /dev/null +++ b/src/constants/ffmpeg/index.ts @@ -0,0 +1,3 @@ +export const INPUT_PATH_PLACEHOLDER = 'INPUT'; +export const FFMPEG_PLACEHOLDER = 'FFMPEG'; +export const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT'; diff --git a/src/constants/file/index.ts b/src/constants/file/index.ts index 93f8eb81b..57c5efab1 100644 --- a/src/constants/file/index.ts +++ b/src/constants/file/index.ts @@ -2,6 +2,7 @@ export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1); export const MAX_EDITED_CREATION_TIME = new Date(); export const MAX_EDITED_FILE_NAME_LENGTH = 100; +export const MAX_CAPTION_SIZE = 5000; export const MAX_TRASH_BATCH_SIZE = 1000; export const TYPE_HEIC = 'heic'; diff --git a/src/constants/pages/index.ts b/src/constants/pages/index.ts index 6cc3ea8b9..b2dc2ac6c 100644 --- a/src/constants/pages/index.ts +++ b/src/constants/pages/index.ts @@ -16,7 +16,3 @@ export enum PAGES { ML_DEBUG = '/ml-debug', DEDUPLICATE = '/deduplicate', } -export const getAlbumSiteHost = () => - process.env.NODE_ENV === 'production' - ? 'albums.ente.io' - : `${window.location.hostname}:3002`; diff --git a/src/components/PhotoSwipe/events.ts b/src/constants/photoViewer/index.ts similarity index 65% rename from src/components/PhotoSwipe/events.ts rename to src/constants/photoViewer/index.ts index b634d17c5..1a22dfeae 100644 --- a/src/components/PhotoSwipe/events.ts +++ b/src/constants/photoViewer/index.ts @@ -1,4 +1,12 @@ -export default [ +export const defaultLivePhotoDefaultOptions = { + click: () => {}, + hide: () => {}, + show: () => {}, + loading: false, + visible: false, +}; + +export const photoSwipeV4Events = [ 'beforeChange', 'afterChange', 'imageLoadComplete', diff --git a/src/constants/photoswipe/index.ts b/src/constants/photoswipe/index.ts deleted file mode 100644 index 7c1d44886..000000000 --- a/src/constants/photoswipe/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const defaultLivePhotoDefaultOptions = { - click: () => {}, - hide: () => {}, - show: () => {}, - loading: false, - visible: false, -}; diff --git a/src/constants/sentry/index.ts b/src/constants/sentry/index.ts index 4e10419f4..abce60083 100644 --- a/src/constants/sentry/index.ts +++ b/src/constants/sentry/index.ts @@ -1,10 +1,15 @@ +export const ENV_DEVELOPMENT = 'development'; +export const ENV_PRODUCTION = 'production'; + export const getSentryDSN = () => process.env.NEXT_PUBLIC_SENTRY_DSN ?? 'https://60abb33b597c42f6a3fb27cd82c55101@sentry.ente.io/2'; export const getSentryENV = () => - process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development'; + process.env.NEXT_PUBLIC_SENTRY_ENV ?? ENV_PRODUCTION; export const getSentryRelease = () => process.env.SENTRY_RELEASE; export { getIsSentryEnabled } from '../../../sentryConfigUtil'; + +export const isDEVSentryENV = () => getSentryENV() === ENV_DEVELOPMENT; diff --git a/src/constants/upload/index.ts b/src/constants/upload/index.ts index b69284f1d..7c4969b93 100644 --- a/src/constants/upload/index.ts +++ b/src/constants/upload/index.ts @@ -12,6 +12,7 @@ export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [ { fileType: FILE_TYPE.IMAGE, exactType: 'jpg', mimeType: 'image/jpeg' }, { fileType: FILE_TYPE.VIDEO, exactType: 'webm', mimeType: 'video/webm' }, { fileType: FILE_TYPE.VIDEO, exactType: 'mod', mimeType: 'video/mpeg' }, + { fileType: FILE_TYPE.VIDEO, exactType: 'mp4', mimeType: 'video/mp4' }, ]; // this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part. @@ -52,6 +53,7 @@ export enum UPLOAD_RESULT { UPLOADED_WITH_STATIC_THUMBNAIL, ADDED_SYMLINK, CANCELLED, + SKIPPED_VIDEOS, } export enum PICKED_UPLOAD_TYPE { @@ -76,6 +78,7 @@ export const USE_CF_PROXY = false; export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = { rootFolderName: '', hasNestedFolders: false, + hasRootLevelFileWithFolder: false, }; export const BLACK_THUMBNAIL_BASE64 = diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 24ecd9fdb..5b046c703 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -8,7 +8,6 @@ import 'photoswipe/dist/photoswipe.css'; import 'styles/global.css'; import EnteSpinner from 'components/EnteSpinner'; import { logError } from '../utils/sentry'; -// import { Workbox } from 'workbox-window'; import { getData, LS_KEYS } from 'utils/storage/localStorage'; import HTTPService from 'services/HTTPService'; import FlashMessageBar from 'components/FlashMessageBar'; @@ -19,7 +18,6 @@ import { getMLSearchConfig, updateMLSearchConfig, } from 'utils/machineLearning/config'; -import { addLogLine, pipeConsoleLogsToDebugLogs } from 'utils/logging'; import LoadingBar from 'react-top-loading-bar'; import DialogBox from 'components/DialogBox'; import { styled, ThemeProvider } from '@mui/material/styles'; @@ -33,6 +31,26 @@ import { getRoadmapRedirectURL, } from 'services/userService'; import { CustomError } from 'utils/error'; +import { + addLogLine, + clearLogsIfLocalStorageLimitExceeded, + pipeConsoleLogsToDebugLogs, +} from 'utils/logging'; +import isElectron from 'is-electron'; +import ElectronUpdateService from 'services/electron/update'; +import { + getUpdateAvailableForDownloadMessage, + getUpdateReadyToInstallMessage, +} from 'utils/ui'; +import Notification from 'components/Notification'; +import { + NotificationAttributes, + SetNotificationAttributes, +} from 'types/Notification'; +import ArrowForward from '@mui/icons-material/ArrowForward'; +import { AppUpdateInfo } from 'types/electron'; +import { getSentryUserID } from 'utils/user'; +import { User } from 'types/user'; export const MessageContainer = styled('div')` background-color: #111; @@ -60,6 +78,7 @@ type AppContextType = { finishLoading: () => void; closeMessageDialog: () => void; setDialogMessage: SetDialogBoxAttributes; + setNotificationAttributes: SetNotificationAttributes; isFolderSyncRunning: boolean; setIsFolderSyncRunning: (isRunning: boolean) => void; watchFolderView: boolean; @@ -107,31 +126,12 @@ export default function App({ Component, err }) { const [watchFolderView, setWatchFolderView] = useState(false); const [watchFolderFiles, setWatchFolderFiles] = useState(null); const isMobile = useMediaQuery('(max-width:428px)'); + const [notificationView, setNotificationView] = useState(false); + const closeNotification = () => setNotificationView(false); + const [notificationAttributes, setNotificationAttributes] = + useState(null); useEffect(() => { - if ( - !('serviceWorker' in navigator) || - process.env.NODE_ENV !== 'production' - ) { - console.warn('Progressive Web App support is disabled'); - } else if ('serviceWorker' in navigator) { - // const wb = new Workbox('sw.js', { scope: '/' }); - // wb.register(); - navigator.serviceWorker.onmessage = (event) => { - if (event.data.action === 'upload-files') { - const files = event.data.files; - setSharedFiles(files); - } - }; - navigator.serviceWorker - .getRegistrations() - .then(function (registrations) { - for (const registration of registrations) { - registration.unregister(); - } - }); - } - HTTPService.getInterceptors().response.use( (resp) => resp, (error) => { @@ -139,7 +139,6 @@ export default function App({ Component, err }) { return Promise.reject(error); } ); - pipeConsoleLogsToDebugLogs(); }, []); useEffect(() => { @@ -153,8 +152,36 @@ export default function App({ Component, err }) { logError(e, 'Error while loading mlSearchEnabled'); } }; - loadMlSearchState(); + clearLogsIfLocalStorageLimitExceeded(); + pipeConsoleLogsToDebugLogs(); + const main = async () => { + addLogLine(`userID: ${(getData(LS_KEYS.USER) as User)?.id}`); + addLogLine(`sentryID: ${await getSentryUserID()}`); + addLogLine(`sentry release ID: ${process.env.SENTRY_RELEASE}`); + }; + main(); + }, []); + + useEffect(() => { + if (isElectron()) { + const showUpdateDialog = (updateInfo: AppUpdateInfo) => { + if (updateInfo.autoUpdatable) { + setDialogMessage(getUpdateReadyToInstallMessage()); + } else { + setNotificationAttributes({ + endIcon: , + variant: 'secondary', + message: constants.UPDATE_AVAILABLE, + onClick: () => + setDialogMessage( + getUpdateAvailableForDownloadMessage(updateInfo) + ), + }); + } + }; + ElectronUpdateService.registerUpdateEventListener(showUpdateDialog); + } }, []); const setUserOnline = () => setOffline(false); @@ -229,10 +256,12 @@ export default function App({ Component, err }) { }, [redirectName]); useEffect(() => { - addLogLine(`app started`); - }, []); + setMessageDialogView(true); + }, [dialogMessage]); - useEffect(() => setMessageDialogView(true), [dialogMessage]); + useEffect(() => { + setNotificationView(true); + }, [notificationAttributes]); const showNavBar = (show: boolean) => setShowNavBar(show); const setDisappearingFlashMessage = (flashMessages: FlashMessage) => { @@ -273,7 +302,7 @@ export default function App({ Component, err }) { - + {showNavbar && } {offline && constants.OFFLINE_MSG} @@ -299,11 +328,17 @@ export default function App({ Component, err }) { + {loading ? ( diff --git a/src/pages/change-password/index.tsx b/src/pages/change-password/index.tsx index d56fadbb9..a71699d94 100644 --- a/src/pages/change-password/index.tsx +++ b/src/pages/change-password/index.tsx @@ -2,10 +2,9 @@ import React, { useState, useEffect } from 'react'; import constants from 'utils/strings/constants'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; import { useRouter } from 'next/router'; -import CryptoWorker, { +import { saveKeyInSessionStore, generateAndSaveIntermediateKeyAttributes, - B64EncryptionResult, } from 'utils/crypto'; import { getActualKey } from 'utils/common/key'; import { setKeys } from 'services/userService'; @@ -14,12 +13,13 @@ import SetPasswordForm, { } from 'components/SetPasswordForm'; import { SESSION_KEYS } from 'utils/storage/sessionStorage'; import { PAGES } from 'constants/pages'; -import { KEK, UpdatedKey, User } from 'types/user'; +import { KEK, KeyAttributes, UpdatedKey, User } from 'types/user'; import LinkButton from 'components/pages/gallery/LinkButton'; import VerticallyCentered from 'components/Container'; import FormPaper from 'components/Form/FormPaper'; import FormPaperFooter from 'components/Form/FormPaper/Footer'; import FormPaperTitle from 'components/Form/FormPaper/Title'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; export default function ChangePassword() { const [token, setToken] = useState(); @@ -40,10 +40,10 @@ export default function ChangePassword() { passphrase, setFieldError ) => { - const cryptoWorker = await new CryptoWorker(); - const key: string = await getActualKey(); - const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); - const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const key = await getActualKey(); + const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); + const kekSalt = await cryptoWorker.generateSaltToDeriveKey(); let kek: KEK; try { kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt); @@ -51,8 +51,10 @@ export default function ChangePassword() { setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED); return; } - const encryptedKeyAttributes: B64EncryptionResult = - await cryptoWorker.encryptToB64(key, kek.key); + const encryptedKeyAttributes = await cryptoWorker.encryptToB64( + key, + kek.key + ); const updatedKey: UpdatedKey = { kekSalt, encryptedKey: encryptedKeyAttributes.encryptedData, diff --git a/src/pages/deduplicate/index.tsx b/src/pages/deduplicate/index.tsx index 2836bb4f0..856b20041 100644 --- a/src/pages/deduplicate/index.tsx +++ b/src/pages/deduplicate/index.tsx @@ -24,6 +24,9 @@ import router from 'next/router'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { styled } from '@mui/material'; import { syncCollections } from 'services/collectionService'; +import EnteSpinner from 'components/EnteSpinner'; +import VerticallyCentered from 'components/Container'; +import { Collection } from 'types/collection'; export const DeduplicateContext = createContext( DefaultDeduplicateContext @@ -42,6 +45,7 @@ export default function Deduplicate() { setRedirectURL, } = useContext(AppContext); const [duplicateFiles, setDuplicateFiles] = useState(null); + const [collections, setCollection] = useState([]); const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false); const [fileSizeMap, setFileSizeMap] = useState(new Map()); const [collectionNameMap, setCollectionNameMap] = useState( @@ -72,6 +76,7 @@ export default function Deduplicate() { const syncWithRemote = async () => { startLoading(); const collections = await syncCollections(); + setCollection(collections); const collectionNameMap = new Map(); for (const collection of collections) { collectionNameMap.set(collection.id, collection.name); @@ -87,11 +92,12 @@ export default function Deduplicate() { let toSelectFileIDs: number[] = []; let count = 0; for (const dupe of duplicates) { - allDuplicateFiles = allDuplicateFiles.concat(dupe.files); + allDuplicateFiles = [...allDuplicateFiles, ...dupe.files]; // select all except first file - toSelectFileIDs = toSelectFileIDs.concat( - dupe.files.slice(1).map((f) => f.id) - ); + toSelectFileIDs = [ + ...toSelectFileIDs, + ...dupe.files.slice(1).map((f) => f.id), + ]; count += dupe.files.length - 1; for (const file of dupe.files) { @@ -143,7 +149,13 @@ export default function Deduplicate() { }; if (!duplicateFiles) { - return <>; + return ( + + + Loading... + + + ); } return ( @@ -165,7 +177,7 @@ export default function Deduplicate() { )} null, setActiveCollection: () => null, syncWithRemote: () => null, - setNotificationAttributes: () => null, setBlockingLoad: () => null, photoListHeader: null, }; @@ -180,7 +178,9 @@ export default function Gallery() { useState(null); const syncInProgress = useRef(true); const resync = useRef(false); - const [deleted, setDeleted] = useState([]); + const [deletedFileIds, setDeletedFileIds] = useState>( + new Set() + ); const { startLoading, finishLoading, setDialogMessage, ...appContext } = useContext(AppContext); const [collectionSummaries, setCollectionSummaries] = @@ -190,13 +190,6 @@ export default function Gallery() { const [fixCreationTimeAttributes, setFixCreationTimeAttributes] = useState(null); - const [notificationView, setNotificationView] = useState(false); - - const closeNotification = () => setNotificationView(false); - - const [notificationAttributes, setNotificationAttributes] = - useState(null); - const [archivedCollections, setArchivedCollections] = useState>(); @@ -232,7 +225,12 @@ export default function Gallery() { router.push(PAGES.ROOT); return; } + preloadImage('/images/subscription-card-background'); const main = async () => { + const valid = await validateKey(); + if (!valid) { + return; + } setActiveCollection(ALL_SECTION); setIsFirstLoad(isFirstLogin()); setIsFirstFetch(true); @@ -241,10 +239,10 @@ export default function Gallery() { } setIsFirstLogin(false); const user = getData(LS_KEYS.USER); - const files = mergeMetadata(await getLocalFiles()); + let files = mergeMetadata(await getLocalFiles()); const collections = await getLocalCollections(); const trash = await getLocalTrash(); - files.push(...getTrashedFiles(trash)); + files = [...files, ...getTrashedFiles(trash)]; setUser(user); setFiles(sortFiles(files)); setCollections(collections); @@ -252,7 +250,6 @@ export default function Gallery() { setIsFirstLoad(false); setJustSignedUp(false); setIsFirstFetch(false); - preloadImage('/images/subscription-card-background'); }; main(); }, []); @@ -264,24 +261,16 @@ export default function Gallery() { setDerivativeState(user, collections, files); }, [collections, files]); - useEffect( - () => collectionSelectorAttributes && setCollectionSelectorView(true), - [collectionSelectorAttributes] - ); + useEffect(() => { + collectionSelectorAttributes && setCollectionSelectorView(true); + }, [collectionSelectorAttributes]); - useEffect( - () => collectionNamerAttributes && setCollectionNamerView(true), - [collectionNamerAttributes] - ); - useEffect( - () => fixCreationTimeAttributes && setFixCreationTimeView(true), - [fixCreationTimeAttributes] - ); - - useEffect( - () => notificationAttributes && setNotificationView(true), - [notificationAttributes] - ); + useEffect(() => { + collectionNamerAttributes && setCollectionNamerView(true); + }, [collectionNamerAttributes]); + useEffect(() => { + fixCreationTimeAttributes && setFixCreationTimeView(true); + }, [fixCreationTimeAttributes]); useEffect(() => { if (typeof activeCollection === 'undefined') { @@ -341,9 +330,9 @@ export default function Gallery() { !silent && startLoading(); const collections = await syncCollections(); setCollections(collections); - const files = await syncFiles(collections, setFiles); + let files = await syncFiles(collections, setFiles); const trash = await syncTrash(collections, setFiles, files); - files.push(...getTrashedFiles(trash)); + files = [...files, ...getTrashedFiles(trash)]; } catch (e) { logError(e, 'syncWithRemote failed'); switch (e.message) { @@ -490,12 +479,12 @@ export default function Gallery() { startLoading(); try { const selectedFiles = getSelectedFiles(selected, files); + setDeletedFileIds((deletedFileIds) => { + selectedFiles.forEach((file) => deletedFileIds.add(file.id)); + return new Set(deletedFileIds); + }); if (permanent) { await deleteFromTrash(selectedFiles.map((file) => file.id)); - setDeleted([ - ...deleted, - ...selectedFiles.map((file) => file.id), - ]); } else { await trashFiles(selectedFiles); } @@ -526,7 +515,6 @@ export default function Gallery() { if (newSearch?.collection) { setActiveCollection(newSearch?.collection); } else { - setActiveCollection(ALL_SECTION); setSearch(newSearch); } if (!newSearch?.collection && !newSearch?.file) { @@ -537,13 +525,6 @@ export default function Gallery() { } }; - const closeCollectionSelector = (closeBtnClick?: boolean) => { - if (closeBtnClick === true) { - appContext.resetSharedFiles(); - } - setCollectionSelectorView(false); - }; - const fixTimeHelper = async () => { const selectedFiles = getSelectedFiles(selected, files); setFixCreationTimeAttributes({ files: selectedFiles }); @@ -567,6 +548,10 @@ export default function Gallery() { setUploadTypeSelectorView(true); }; + const closeCollectionSelector = () => { + setCollectionSelectorView(false); + }; + return ( @@ -602,11 +586,6 @@ export default function Gallery() { closeModal={() => setPlanModalView(false)} setLoading={setBlockingLoad} /> - @@ -685,10 +664,9 @@ export default function Gallery() { sidebarView={sidebarView} closeSidebar={closeSidebar} /> - { appContext.showNavBar(false); const currentURL = new URL(window.location.href); - const ALBUM_SITE_HOST = getAlbumSiteHost(); + const albumsURL = new URL(getAlbumsURL()); currentURL.pathname = router.pathname; if ( - currentURL.host === ALBUM_SITE_HOST && + currentURL.host === albumsURL.host && currentURL.pathname !== PAGES.SHARED_ALBUMS ) { handleAlbumsRedirect(currentURL); diff --git a/src/pages/recover/index.tsx b/src/pages/recover/index.tsx index 78a4865d7..67c4ecd36 100644 --- a/src/pages/recover/index.tsx +++ b/src/pages/recover/index.tsx @@ -8,15 +8,11 @@ import { } from 'utils/storage/localStorage'; import { useRouter } from 'next/router'; import { PAGES } from 'constants/pages'; -import CryptoWorker, { - decryptAndStoreToken, - saveKeyInSessionStore, -} from 'utils/crypto'; +import { decryptAndStoreToken, saveKeyInSessionStore } from 'utils/crypto'; import SingleInputForm, { SingleInputFormProps, } from 'components/SingleInputForm'; import VerticallyCentered from 'components/Container'; -import { Button } from 'react-bootstrap'; import { AppContext } from 'pages/_app'; import { logError } from 'utils/sentry'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; @@ -24,6 +20,8 @@ import { KeyAttributes, User } from 'types/user'; import FormPaper from 'components/Form/FormPaper'; import FormPaperTitle from 'components/Form/FormPaper/Title'; import FormPaperFooter from 'components/Form/FormPaper/Footer'; +import LinkButton from 'components/pages/gallery/LinkButton'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; const bip39 = require('bip39'); // mobile client library only supports english. bip39.setDefaultWordlist('english'); @@ -59,15 +57,21 @@ export default function Recover() { setFieldError ) => { try { + recoveryKey = recoveryKey + .trim() + .split(' ') + .map((part) => part.trim()) + .filter((part) => !!part) + .join(' '); // check if user is entering mnemonic recovery key - if (recoveryKey.trim().indexOf(' ') > 0) { - if (recoveryKey.trim().split(' ').length !== 24) { + if (recoveryKey.indexOf(' ') > 0) { + if (recoveryKey.split(' ').length !== 24) { throw new Error('recovery code should have 24 words'); } recoveryKey = bip39.mnemonicToEntropy(recoveryKey); } - const cryptoWorker = await new CryptoWorker(); - const masterKey: string = await cryptoWorker.decryptB64( + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const masterKey = await cryptoWorker.decryptB64( keyAttributes.masterKeyEncryptedWithRecoveryKey, keyAttributes.masterKeyDecryptionNonce, await cryptoWorker.fromHex(recoveryKey) @@ -102,12 +106,12 @@ export default function Recover() { buttonText={constants.RECOVER} /> - - + diff --git a/src/pages/shared-albums/index.tsx b/src/pages/shared-albums/index.tsx index a557764a2..0425dee73 100644 --- a/src/pages/shared-albums/index.tsx +++ b/src/pages/shared-albums/index.tsx @@ -18,15 +18,11 @@ import { EnteFile } from 'types/file'; import { mergeMetadata, sortFiles } from 'utils/file'; import { AppContext } from 'pages/_app'; import { AbuseReportForm } from 'components/pages/sharedAlbum/AbuseReportForm'; -import { - defaultPublicCollectionGalleryContext, - PublicCollectionGalleryContext, -} from 'utils/publicCollectionGallery'; +import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { CustomError, parseSharingErrorCodes } from 'utils/error'; -import VerticallyCentered from 'components/Container'; +import VerticallyCentered, { CenteredFlex } from 'components/Container'; import constants from 'utils/strings/constants'; import EnteSpinner from 'components/EnteSpinner'; -import CryptoWorker from 'utils/crypto'; import { PAGES } from 'constants/pages'; import { useRouter } from 'next/router'; import SingleInputForm, { @@ -41,6 +37,17 @@ import FormContainer from 'components/Form/FormContainer'; import FormPaper from 'components/Form/FormPaper'; import FormPaperTitle from 'components/Form/FormPaper/Title'; import Typography from '@mui/material/Typography'; +import Uploader from 'components/Upload/Uploader'; +import { LoadingOverlay } from 'components/LoadingOverlay'; +import FullScreenDropZone from 'components/FullScreenDropZone'; +import useFileInput from 'hooks/useFileInput'; +import { useDropzone } from 'react-dropzone'; +import UploadSelectorInputs from 'components/UploadSelectorInputs'; +import { logoutUser } from 'services/userService'; +import UploadButton from 'components/Upload/UploadButton'; +import bs58 from 'bs58'; +import AddPhotoAlternateOutlined from '@mui/icons-material/AddPhotoAlternateOutlined'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; const Loader = () => ( @@ -49,7 +56,6 @@ const Loader = () => ( ); -const bs58 = require('bs58'); export default function PublicCollectionGallery() { const token = useRef(null); // passwordJWTToken refers to the jwt token which is used for album protected by password. @@ -58,7 +64,7 @@ export default function PublicCollectionGallery() { const url = useRef(null); const [publicFiles, setPublicFiles] = useState(null); const [publicCollection, setPublicCollection] = useState(null); - const [errorMessage, setErrorMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); const appContext = useContext(AppContext); const [abuseReportFormView, setAbuseReportFormView] = useState(false); const [loading, setLoading] = useState(true); @@ -70,6 +76,58 @@ export default function PublicCollectionGallery() { const [photoListHeader, setPhotoListHeader] = useState(null); + const [photoListFooter, setPhotoListFooter] = + useState(null); + + const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false); + const [blockingLoad, setBlockingLoad] = useState(false); + const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false); + + const { + getRootProps: getDragAndDropRootProps, + getInputProps: getDragAndDropInputProps, + acceptedFiles: dragAndDropFiles, + } = useDropzone({ + noClick: true, + noKeyboard: true, + disabled: shouldDisableDropzone, + }); + const { + selectedFiles: webFileSelectorFiles, + open: openFileSelector, + getInputProps: getFileSelectorInputProps, + } = useFileInput({ + directory: false, + }); + const { + selectedFiles: webFolderSelectorFiles, + open: openFolderSelector, + getInputProps: getFolderSelectorInputProps, + } = useFileInput({ + directory: true, + }); + + const openUploader = () => { + setUploadTypeSelectorView(true); + }; + + const closeUploadTypeSelectorView = () => { + setUploadTypeSelectorView(false); + }; + + const showPublicLinkExpiredMessage = () => + appContext.setDialogMessage({ + title: constants.LINK_EXPIRED, + content: constants.LINK_EXPIRED_MESSAGE, + + nonClosable: true, + proceed: { + text: constants.LOGIN, + action: logoutUser, + variant: 'accent', + }, + }); + useEffect(() => { const currentURL = new URL(window.location.href); if (currentURL.pathname !== PAGES.ROOT) { @@ -91,7 +149,8 @@ export default function PublicCollectionGallery() { } const main = async () => { try { - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + url.current = window.location.href; const currentURL = new URL(url.current); const t = currentURL.searchParams.get('t'); @@ -101,8 +160,8 @@ export default function PublicCollectionGallery() { } const dck = ck.length < 50 - ? await worker.toB64(bs58.decode(ck)) - : await worker.fromHex(ck); + ? await cryptoWorker.toB64(bs58.decode(ck)) + : await cryptoWorker.fromHex(ck); token.current = t; collectionKey.current = dck; url.current = window.location.href; @@ -111,6 +170,9 @@ export default function PublicCollectionGallery() { ); if (localCollection) { setPublicCollection(localCollection); + const isPasswordProtected = + localCollection?.publicURLs?.[0]?.passwordEnabled; + setIsPasswordProtected(isPasswordProtected); const collectionUID = getPublicCollectionUID(token.current); const localFiles = await getLocalPublicFiles(collectionUID); const localPublicFiles = sortFiles( @@ -128,9 +190,8 @@ export default function PublicCollectionGallery() { main(); }, []); - useEffect( - () => - publicCollection && + useEffect(() => { + publicCollection && publicFiles && setPhotoListHeader({ item: ( @@ -143,14 +204,36 @@ export default function PublicCollectionGallery() { ), itemType: ITEM_TYPE.OTHER, height: 68, - }), - [publicCollection, publicFiles] - ); + }); + }, [publicCollection, publicFiles]); + + useEffect(() => { + if (publicCollection?.publicURLs?.[0]?.enableCollect) { + setPhotoListFooter({ + item: ( + + } + /> + + ), + itemType: ITEM_TYPE.OTHER, + height: 104, + }); + } else { + setPhotoListFooter(null); + } + }, [publicCollection]); const syncWithRemote = async () => { const collectionUID = getPublicCollectionUID(token.current); try { appContext.startLoading(); + setLoading(true); const collection = await getPublicCollection( token.current, collectionKey.current @@ -198,7 +281,7 @@ export default function PublicCollectionGallery() { setErrorMessage( parsedError.message === CustomError.TOO_MANY_REQUESTS ? constants.LINK_TOO_MANY_REQUESTS - : constants.LINK_EXPIRED + : constants.LINK_EXPIRED_MESSAGE ); // share has been disabled // local cache should be cleared @@ -213,6 +296,7 @@ export default function PublicCollectionGallery() { } } finally { appContext.finishLoading(); + setLoading(false); } }; @@ -221,7 +305,7 @@ export default function PublicCollectionGallery() { setFieldError ) => { try { - const cryptoWorker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); let hashedPassword: string = null; try { const publicUrl = publicCollection.publicURLs[0]; @@ -299,37 +383,66 @@ export default function PublicCollectionGallery() { return ( - - null} - selected={{ count: 0, collectionID: null }} - isFirstLoad={true} - openUploader={() => null} - isInSearchMode={false} - search={{}} - deleted={[]} - activeCollection={ALL_SECTION} - isSharedCollection - enableDownload={ - publicCollection?.publicURLs?.[0]?.enableDownload ?? true - } - /> - + + + + null} + selected={{ count: 0, collectionID: null }} + isFirstLoad={true} + activeCollection={ALL_SECTION} + isSharedCollection + enableDownload={ + publicCollection?.publicURLs?.[0]?.enableDownload ?? + true + } + /> + + {blockingLoad && ( + + + + )} + + ); } diff --git a/src/pages/two-factor/recover/index.tsx b/src/pages/two-factor/recover/index.tsx index a090e33cb..9503162f0 100644 --- a/src/pages/two-factor/recover/index.tsx +++ b/src/pages/two-factor/recover/index.tsx @@ -2,12 +2,10 @@ import React, { useContext, useEffect, useState } from 'react'; import constants from 'utils/strings/constants'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; import { useRouter } from 'next/router'; -import CryptoWorker, { B64EncryptionResult } from 'utils/crypto'; import SingleInputForm, { SingleInputFormProps, } from 'components/SingleInputForm'; import VerticallyCentered from 'components/Container'; -import { Button } from 'react-bootstrap'; import { logError } from 'utils/sentry'; import { recoverTwoFactor, removeTwoFactor } from 'services/userService'; import { AppContext } from 'pages/_app'; @@ -15,6 +13,9 @@ import { PAGES } from 'constants/pages'; import FormPaper from 'components/Form/FormPaper'; import FormPaperTitle from 'components/Form/FormPaper/Title'; import FormPaperFooter from 'components/Form/FormPaper/Footer'; +import LinkButton from 'components/pages/gallery/LinkButton'; +import { B64EncryptionResult } from 'types/crypto'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; const bip39 = require('bip39'); // mobile client library only supports english. bip39.setDefaultWordlist('english'); @@ -52,15 +53,21 @@ export default function Recover() { setFieldError ) => { try { + recoveryKey = recoveryKey + .trim() + .split(' ') + .map((part) => part.trim()) + .filter((part) => !!part) + .join(' '); // check if user is entering mnemonic recovery key - if (recoveryKey.trim().indexOf(' ') > 0) { - if (recoveryKey.trim().split(' ').length !== 24) { + if (recoveryKey.indexOf(' ') > 0) { + if (recoveryKey.split(' ').length !== 24) { throw new Error('recovery code should have 24 words'); } recoveryKey = bip39.mnemonicToEntropy(recoveryKey); } - const cryptoWorker = await new CryptoWorker(); - const twoFactorSecret: string = await cryptoWorker.decryptB64( + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const twoFactorSecret = await cryptoWorker.decryptB64( encryptedTwoFactorSecret.encryptedData, encryptedTwoFactorSecret.nonce, await cryptoWorker.fromHex(recoveryKey) @@ -101,12 +108,12 @@ export default function Recover() { buttonText={constants.RECOVER} /> - - + diff --git a/src/serviceWorker.js b/src/serviceWorker.js deleted file mode 100644 index aed91c3fa..000000000 --- a/src/serviceWorker.js +++ /dev/null @@ -1,34 +0,0 @@ -import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching'; -import { registerRoute, setDefaultHandler } from 'workbox-routing'; -import { NetworkOnly } from 'workbox-strategies'; -import { pageCache, offlineFallback } from 'workbox-recipes'; - -pageCache(); - -precacheAndRoute(self.__WB_MANIFEST); -cleanupOutdatedCaches(); - -registerRoute( - '/share-target', - async ({ event }) => { - event.waitUntil( - (async function () { - const data = await event.request.formData(); - const client = await self.clients.get( - event.resultingClientId || event.clientId - ); - const files = data.getAll('files'); - setTimeout(() => { - client.postMessage({ files, action: 'upload-files' }); - }, 1000); - })() - ); - return Response.redirect('./'); - }, - 'POST' -); - -// Use a stale-while-revalidate strategy for all other requests. -setDefaultHandler(new NetworkOnly()); - -offlineFallback(); diff --git a/src/services/billingService.ts b/src/services/billingService.ts index 5d95e345a..19807bd16 100644 --- a/src/services/billingService.ts +++ b/src/services/billingService.ts @@ -6,7 +6,7 @@ import { logError } from 'utils/sentry'; import { getPaymentToken } from './userService'; import { Plan, Subscription } from 'types/billing'; import isElectron from 'is-electron'; -import { DESKTOP_REDIRECT_URL } from 'constants/billing'; +import { getDesktopRedirectURL } from 'constants/billing'; const ENDPOINT = getEndpoint(); @@ -170,12 +170,7 @@ class billingService { action: string ) { try { - let redirectURL; - if (isElectron()) { - redirectURL = DESKTOP_REDIRECT_URL; - } else { - redirectURL = `${window.location.origin}/gallery`; - } + const redirectURL = this.getRedirectURL(); window.location.href = `${getPaymentsURL()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`; } catch (e) { logError(e, 'unable to get payments url'); @@ -185,9 +180,10 @@ class billingService { public async redirectToCustomerPortal() { try { + const redirectURL = this.getRedirectURL(); const response = await HTTPService.get( `${ENDPOINT}/billing/stripe/customer-portal`, - { redirectURL: `${window.location.origin}/gallery` }, + { redirectURL }, { 'X-Auth-Token': getToken(), } @@ -198,6 +194,14 @@ class billingService { throw e; } } + + public getRedirectURL() { + if (isElectron()) { + return getDesktopRedirectURL(); + } else { + return `${window.location.origin}/gallery`; + } + } } export default new billingService(); diff --git a/src/services/collectionService.ts b/src/services/collectionService.ts index 8188b8914..ddc44c902 100644 --- a/src/services/collectionService.ts +++ b/src/services/collectionService.ts @@ -3,9 +3,7 @@ import { getData, LS_KEYS } from 'utils/storage/localStorage'; import localForage from 'utils/storage/localForage'; import { getActualKey, getToken } from 'utils/common/key'; -import CryptoWorker from 'utils/crypto'; import { getPublicKey } from './userService'; -import { B64EncryptionResult } from 'utils/crypto'; import HTTPService from './HTTPService'; import { EnteFile } from 'types/file'; import { logError } from 'utils/sentry'; @@ -28,6 +26,9 @@ import { CollectionSummaries, CollectionSummary, CollectionFilesCount, + EncryptedCollection, + CollectionMagicMetadata, + CollectionMagicMetadataProps, } from 'types/collection'; import { COLLECTION_SORT_BY, @@ -38,60 +39,74 @@ import { ALL_SECTION, CollectionSummaryType, } from 'constants/collection'; -import { UpdateMagicMetadataRequest } from 'types/magicMetadata'; -import { EncryptionResult } from 'types/upload'; +import { + NEW_COLLECTION_MAGIC_METADATA, + SUB_TYPE, + UpdateMagicMetadataRequest, +} from 'types/magicMetadata'; import constants from 'utils/strings/constants'; -import { IsArchived } from 'utils/magicMetadata'; +import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata'; import { User } from 'types/user'; +import { + getNonHiddenCollections, + isQuickLinkCollection, +} from 'utils/collection'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; const ENDPOINT = getEndpoint(); const COLLECTION_TABLE = 'collections'; const COLLECTION_UPDATION_TIME = 'collection-updation-time'; const getCollectionWithSecrets = async ( - collection: Collection, + collection: EncryptedCollection, masterKey: string -) => { - const worker = await new CryptoWorker(); +): Promise => { + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const userID = getData(LS_KEYS.USER).id; - let decryptedKey: string; + let collectionKey: string; if (collection.owner.id === userID) { - decryptedKey = await worker.decryptB64( + collectionKey = await cryptoWorker.decryptB64( collection.encryptedKey, collection.keyDecryptionNonce, masterKey ); } else { const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); - const secretKey = await worker.decryptB64( + const secretKey = await cryptoWorker.decryptB64( keyAttributes.encryptedSecretKey, keyAttributes.secretKeyDecryptionNonce, masterKey ); - decryptedKey = await worker.boxSealOpen( + collectionKey = await cryptoWorker.boxSealOpen( collection.encryptedKey, keyAttributes.publicKey, secretKey ); } - collection.name = + const collectionName = collection.name || - (await worker.decryptToUTF8( + (await cryptoWorker.decryptToUTF8( collection.encryptedName, collection.nameDecryptionNonce, - decryptedKey + collectionKey )); + let collectionMagicMetadata: CollectionMagicMetadata; if (collection.magicMetadata?.data) { - collection.magicMetadata.data = await worker.decryptMetadata( - collection.magicMetadata.data, - collection.magicMetadata.header, - decryptedKey - ); + collectionMagicMetadata = { + ...collection.magicMetadata, + data: await cryptoWorker.decryptMetadata( + collection.magicMetadata.data, + collection.magicMetadata.header, + collectionKey + ), + }; } return { ...collection, - key: decryptedKey, + name: collectionName, + key: collectionKey, + magicMetadata: collectionMagicMetadata, }; }; @@ -108,27 +123,25 @@ const getCollections = async ( }, { 'X-Auth-Token': token } ); - const promises: Promise[] = resp.data.collections.map( - async (collection: Collection) => { - if (collection.isDeleted) { - return collection; + const decryptedCollections: Collection[] = await Promise.all( + resp.data.collections.map( + async (collection: EncryptedCollection) => { + if (collection.isDeleted) { + return collection; + } + try { + return await getCollectionWithSecrets(collection, key); + } catch (e) { + logError(e, `decryption failed for collection`, { + collectionID: collection.id, + }); + return collection; + } } - let collectionWithSecrets = collection; - try { - collectionWithSecrets = await getCollectionWithSecrets( - collection, - key - ); - } catch (e) { - logError(e, `decryption failed for collection`, { - collectionID: collection.id, - }); - } - return collectionWithSecrets; - } + ) ); // only allow deleted or collection with key, filtering out collection whose decryption failed - const collections = (await Promise.all(promises)).filter( + const collections = decryptedCollections.filter( (collection) => collection.isDeleted || collection.key ); return collections; @@ -141,7 +154,7 @@ const getCollections = async ( export const getLocalCollections = async (): Promise => { const collections: Collection[] = (await localForage.getItem(COLLECTION_TABLE)) ?? []; - return collections; + return getNonHiddenCollections(collections); }; export const getCollectionUpdationTime = async (): Promise => @@ -186,7 +199,7 @@ export const syncCollections = async () => { await localForage.setItem(COLLECTION_TABLE, collections); await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime); - return collections; + return getNonHiddenCollections(collections); }; export const getCollection = async ( @@ -273,25 +286,15 @@ export const createCollection = async ( return collection; } } - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const encryptionKey = await getActualKey(); const token = getToken(); - const collectionKey: string = await worker.generateEncryptionKey(); - const { - encryptedData: encryptedKey, - nonce: keyDecryptionNonce, - }: B64EncryptionResult = await worker.encryptToB64( - collectionKey, - encryptionKey - ); - const { - encryptedData: encryptedName, - nonce: nameDecryptionNonce, - }: B64EncryptionResult = await worker.encryptUTF8( - collectionName, - collectionKey - ); - const newCollection: Collection = { + const collectionKey = await cryptoWorker.generateEncryptionKey(); + const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } = + await cryptoWorker.encryptToB64(collectionKey, encryptionKey); + const { encryptedData: encryptedName, nonce: nameDecryptionNonce } = + await cryptoWorker.encryptUTF8(collectionName, collectionKey); + const newCollection: EncryptedCollection = { id: null, owner: null, encryptedKey, @@ -305,15 +308,12 @@ export const createCollection = async ( isDeleted: false, magicMetadata: null, }; - let createdCollection: Collection = await postCollection( - newCollection, - token - ); - createdCollection = await getCollectionWithSecrets( + const createdCollection = await postCollection(newCollection, token); + const decryptedCreatedCollection = await getCollectionWithSecrets( createdCollection, encryptionKey ); - return createdCollection; + return decryptedCreatedCollection; } catch (e) { logError(e, 'create collection failed'); throw e; @@ -321,9 +321,9 @@ export const createCollection = async ( }; const postCollection = async ( - collectionData: Collection, + collectionData: EncryptedCollection, token: string -): Promise => { +): Promise => { try { const response = await HTTPService.post( `${ENDPOINT}/collections`, @@ -456,19 +456,19 @@ const encryptWithNewCollectionKey = async ( files: EnteFile[] ): Promise => { const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = []; - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); for (const file of files) { - const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64( + const newEncryptedKey = await cryptoWorker.encryptToB64( file.key, newCollection.key ); - file.encryptedKey = newEncryptedKey.encryptedData; - file.keyDecryptionNonce = newEncryptedKey.nonce; + const encryptedKey = newEncryptedKey.encryptedData; + const keyDecryptionNonce = newEncryptedKey.nonce; fileKeysEncryptedWithNewCollection.push({ id: file.id, - encryptedKey: file.encryptedKey, - keyDecryptionNonce: file.keyDecryptionNonce, + encryptedKey, + keyDecryptionNonce, }); } return fileKeysEncryptedWithNewCollection; @@ -512,26 +512,41 @@ export const deleteCollection = async (collectionID: number) => { } }; +export const leaveSharedAlbum = async (collectionID: number) => { + try { + const token = getToken(); + + await HTTPService.post( + `${ENDPOINT}/collections/leave/${collectionID}`, + null, + null, + { 'X-Auth-Token': token } + ); + } catch (e) { + logError(e, constants.LEAVE_SHARED_ALBUM_FAILED); + throw e; + } +}; + export const updateCollectionMagicMetadata = async (collection: Collection) => { const token = getToken(); if (!token) { return; } - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const { file: encryptedMagicMetadata }: EncryptionResult = - await worker.encryptMetadata( - collection.magicMetadata.data, - collection.key - ); + const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata( + collection.magicMetadata.data, + collection.key + ); const reqBody: UpdateMagicMetadataRequest = { id: collection.id, magicMetadata: { version: collection.magicMetadata.version, count: collection.magicMetadata.count, - data: encryptedMagicMetadata.encryptedData as unknown as string, + data: encryptedMagicMetadata.encryptedData, header: encryptedMagicMetadata.decryptionHeader, }, }; @@ -558,15 +573,14 @@ export const renameCollection = async ( collection: Collection, newCollectionName: string ) => { + if (isQuickLinkCollection(collection)) { + // Convert quick link collction to normal collection on rename + await updateCollectionSubType(collection, SUB_TYPE.DEFAULT); + } const token = getToken(); - const worker = await new CryptoWorker(); - const { - encryptedData: encryptedName, - nonce: nameDecryptionNonce, - }: B64EncryptionResult = await worker.encryptUTF8( - newCollectionName, - collection.key - ); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const { encryptedData: encryptedName, nonce: nameDecryptionNonce } = + await cryptoWorker.encryptUTF8(newCollectionName, collection.key); const collectionRenameRequest = { collectionID: collection.id, encryptedName, @@ -581,16 +595,34 @@ export const renameCollection = async ( } ); }; + +const updateCollectionSubType = async ( + collection: Collection, + subType: SUB_TYPE +) => { + const updatedMagicMetadataProps: CollectionMagicMetadataProps = { + subType: subType, + }; + const updatedCollection = { + ...collection, + magicMetadata: await updateMagicMetadataProps( + collection.magicMetadata ?? NEW_COLLECTION_MAGIC_METADATA, + collection.key, + updatedMagicMetadataProps + ), + } as Collection; + await updateCollectionMagicMetadata(updatedCollection); +}; + export const shareCollection = async ( collection: Collection, withUserEmail: string ) => { try { - const worker = await new CryptoWorker(); - + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const token = getToken(); const publicKey: string = await getPublicKey(withUserEmail); - const encryptedKey: string = await worker.boxSeal( + const encryptedKey = await cryptoWorker.boxSeal( collection.key, publicKey ); @@ -735,11 +767,6 @@ export function sortCollectionSummaries( return collectionSummaries .sort((a, b) => { switch (sortBy) { - case COLLECTION_SORT_BY.CREATION_TIME_DESCENDING: - return compareCollectionsLatestFile( - b.latestFile, - a.latestFile - ); case COLLECTION_SORT_BY.CREATION_TIME_ASCENDING: return ( -1 * diff --git a/src/services/deduplicationService.ts b/src/services/deduplicationService.ts index 9ab0e0b12..50d2bd7a9 100644 --- a/src/services/deduplicationService.ts +++ b/src/services/deduplicationService.ts @@ -33,7 +33,7 @@ export async function getDuplicateFiles( fileMap.set(file.id, file); } - const result: DuplicateFiles[] = []; + let result: DuplicateFiles[] = []; for (const dupe of dupes) { let duplicateFiles: EnteFile[] = []; @@ -48,12 +48,13 @@ export async function getDuplicateFiles( ); if (duplicateFiles.length > 1) { - result.push( + result = [ + ...result, ...getDupesGroupedBySameFileHashes( duplicateFiles, dupe.size - ) - ); + ), + ]; } } diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index 7808b3c17..2045dcd59 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -1,6 +1,5 @@ import { getToken } from 'utils/common/key'; import { getFileURL, getThumbnailURL } from 'utils/common/apiUtil'; -import CryptoWorker from 'utils/crypto'; import { generateStreamFromArrayBuffer, getRenderableFileURL, @@ -13,11 +12,23 @@ import { FILE_TYPE } from 'constants/file'; import { CustomError } from 'utils/error'; import { THUMB_CACHE } from 'constants/cache'; import { CacheStorageService } from './cache/cacheStorageService'; +import QueueProcessor, { PROCESSING_STRATEGY } from './queueProcessor'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; + +const MAX_PARALLEL_DOWNLOADS = 10; class DownloadManager { - private fileObjectURLPromise = new Map>(); + private fileObjectURLPromise = new Map< + string, + Promise<{ original: string[]; converted: string[] }> + >(); private thumbnailObjectURLPromise = new Map>(); + private thumbnailDownloadRequestsProcessor = new QueueProcessor( + MAX_PARALLEL_DOWNLOADS, + PROCESSING_STRATEGY.LIFO + ); + public async getThumbnail( file: EnteFile, tokenOverride?: string, @@ -40,11 +51,10 @@ class DownloadManager { if (cacheResp) { return URL.createObjectURL(await cacheResp.blob()); } - const thumb = await this.downloadThumb( - token, - file, - timeout - ); + const thumb = + await this.thumbnailDownloadRequestsProcessor.queueUpRequest( + () => this.downloadThumb(token, file, timeout) + ).promise; const thumbBlob = new Blob([thumb]); thumbnailCache @@ -76,10 +86,10 @@ class DownloadManager { if (typeof resp.data === 'undefined') { throw Error(CustomError.REQUEST_FAILED); } - const worker = await new CryptoWorker(); - const decrypted: Uint8Array = await worker.decryptThumbnail( + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const decrypted = await cryptoWorker.decryptThumbnail( new Uint8Array(resp.data), - await worker.fromB64(file.thumbnail.decryptionHeader), + await cryptoWorker.fromB64(file.thumbnail.decryptionHeader), file.key ); return decrypted; @@ -94,12 +104,11 @@ class DownloadManager { if (forPreview) { return await getRenderableFileURL(file, fileBlob); } else { - return [ - await createTypedObjectURL( - fileBlob, - file.metadata.title - ), - ]; + const fileURL = await createTypedObjectURL( + fileBlob, + file.metadata.title + ); + return { converted: [fileURL], original: [fileURL] }; } }; if (!this.fileObjectURLPromise.get(fileKey)) { @@ -124,7 +133,8 @@ class DownloadManager { usingWorker?: any, timeout?: number ) { - const worker = usingWorker || (await new CryptoWorker()); + const cryptoWorker = + usingWorker || (await ComlinkCryptoWorker.getInstance()); const token = tokenOverride || getToken(); if (!token) { return null; @@ -142,9 +152,9 @@ class DownloadManager { if (typeof resp.data === 'undefined') { throw Error(CustomError.REQUEST_FAILED); } - const decrypted: any = await worker.decryptFile( + const decrypted = await cryptoWorker.decryptFile( new Uint8Array(resp.data), - await worker.fromB64(file.file.decryptionHeader), + await cryptoWorker.fromB64(file.file.decryptionHeader), file.key ); return generateStreamFromArrayBuffer(decrypted); @@ -157,12 +167,15 @@ class DownloadManager { const reader = resp.body.getReader(); const stream = new ReadableStream({ async start(controller) { - const decryptionHeader = await worker.fromB64( + const decryptionHeader = await cryptoWorker.fromB64( file.file.decryptionHeader ); - const fileKey = await worker.fromB64(file.key); + const fileKey = await cryptoWorker.fromB64(file.key); const { pullState, decryptionChunkSize } = - await worker.initDecryption(decryptionHeader, fileKey); + await cryptoWorker.initDecryption( + decryptionHeader, + fileKey + ); let data = new Uint8Array(); // The following function handles each data chunk function push() { @@ -181,7 +194,7 @@ class DownloadManager { decryptionChunkSize ); const { decryptedData } = - await worker.decryptChunk( + await cryptoWorker.decryptChunk( fileData, pullState ); @@ -194,7 +207,10 @@ class DownloadManager { } else { if (data) { const { decryptedData } = - await worker.decryptChunk(data, pullState); + await cryptoWorker.decryptChunk( + data, + pullState + ); controller.enqueue(decryptedData); data = null; } diff --git a/src/services/electron/common.ts b/src/services/electron/common.ts index 7b0d65854..baf27f33f 100644 --- a/src/services/electron/common.ts +++ b/src/services/electron/common.ts @@ -3,15 +3,36 @@ import { ElectronAPIs } from 'types/electron'; class ElectronService { private electronAPIs: ElectronAPIs; - private isBundledApp: boolean = false; constructor() { this.electronAPIs = globalThis['ElectronAPIs']; - this.isBundledApp = !!this.electronAPIs?.openDiskCache; } checkIsBundledApp() { - return isElectron() && this.isBundledApp; + return isElectron() && !!this.electronAPIs?.openDiskCache; + } + + logToDisk(msg: string) { + if (this.electronAPIs?.logToDisk) { + this.electronAPIs.logToDisk(msg); + } + } + + openLogDirectory() { + if (this.electronAPIs?.openLogDirectory) { + this.electronAPIs.openLogDirectory(); + } + } + + getSentryUserID() { + if (this.electronAPIs?.getSentryUserID) { + return this.electronAPIs.getSentryUserID(); + } + } + getAppVersion() { + if (this.electronAPIs?.getAppVersion) { + return this.electronAPIs.getAppVersion(); + } } } diff --git a/src/services/electron/ffmpeg.ts b/src/services/electron/ffmpeg.ts new file mode 100644 index 000000000..b16133d24 --- /dev/null +++ b/src/services/electron/ffmpeg.ts @@ -0,0 +1,26 @@ +import { IFFmpeg } from 'services/ffmpeg/ffmpegFactory'; +import { ElectronAPIs } from 'types/electron'; +import { ElectronFile } from 'types/upload'; +import { runningInBrowser } from 'utils/common'; + +export class ElectronFFmpeg implements IFFmpeg { + private electronAPIs: ElectronAPIs; + + constructor() { + this.electronAPIs = runningInBrowser() && globalThis['ElectronAPIs']; + } + + async run( + cmd: string[], + inputFile: ElectronFile | File, + outputFilename: string + ) { + if (this.electronAPIs?.runFFmpegCmd) { + return this.electronAPIs.runFFmpegCmd( + cmd, + inputFile, + outputFilename + ); + } + } +} diff --git a/src/services/electron/imageProcessor.ts b/src/services/electron/imageProcessor.ts new file mode 100644 index 000000000..9f8a84aa3 --- /dev/null +++ b/src/services/electron/imageProcessor.ts @@ -0,0 +1,77 @@ +import { ElectronAPIs } from 'types/electron'; +import { ElectronFile } from 'types/upload'; +import { convertBytesToHumanReadable } from 'utils/file/size'; +import { addLogLine } from 'utils/logging'; +import { logError } from 'utils/sentry'; + +class ElectronImageProcessorService { + private electronAPIs: ElectronAPIs; + constructor() { + this.electronAPIs = globalThis['ElectronAPIs']; + } + + convertAPIExists() { + return !!this.electronAPIs?.convertHEIC; + } + + generateImageThumbnailAPIExists() { + return !!this.electronAPIs?.generateImageThumbnail; + } + + async convertHEIC(fileBlob: Blob): Promise { + try { + if (!this.electronAPIs?.convertHEIC) { + throw new Error('convertHEIC API not available'); + } + const startTime = Date.now(); + const inputFileData = new Uint8Array(await fileBlob.arrayBuffer()); + const convertedFileData = await this.electronAPIs.convertHEIC( + inputFileData + ); + addLogLine( + `originalFileSize:${convertBytesToHumanReadable( + fileBlob?.size + )},convertedFileSize:${convertBytesToHumanReadable( + convertedFileData?.length + )}, native heic conversion time: ${Date.now() - startTime}ms ` + ); + return new Blob([convertedFileData]); + } catch (e) { + logError(e, 'failed to convert heic natively'); + throw e; + } + } + + async generateImageThumbnail( + inputFile: File | ElectronFile, + maxDimension: number, + maxSize: number + ): Promise { + try { + if (!this.electronAPIs?.generateImageThumbnail) { + throw new Error('generateImageThumbnail API not available'); + } + const startTime = Date.now(); + const thumb = await this.electronAPIs.generateImageThumbnail( + inputFile, + maxDimension, + maxSize + ); + addLogLine( + `originalFileSize:${convertBytesToHumanReadable( + inputFile?.size + )},thumbFileSize:${convertBytesToHumanReadable( + thumb?.length + )}, native thumbnail generation time: ${ + Date.now() - startTime + }ms ` + ); + return thumb; + } catch (e) { + logError(e, 'failed to generate image thumbnail natively'); + throw e; + } + } +} + +export default new ElectronImageProcessorService(); diff --git a/src/services/electron/update.ts b/src/services/electron/update.ts new file mode 100644 index 000000000..121facfd1 --- /dev/null +++ b/src/services/electron/update.ts @@ -0,0 +1,31 @@ +import { AppUpdateInfo, ElectronAPIs } from 'types/electron'; + +class ElectronUpdateService { + private electronAPIs: ElectronAPIs; + + constructor() { + this.electronAPIs = globalThis['ElectronAPIs']; + } + + registerUpdateEventListener( + showUpdateDialog: (updateInfo: AppUpdateInfo) => void + ) { + if (this.electronAPIs?.registerUpdateEventListener) { + this.electronAPIs.registerUpdateEventListener(showUpdateDialog); + } + } + + updateAndRestart() { + if (this.electronAPIs?.updateAndRestart) { + this.electronAPIs.updateAndRestart(); + } + } + + skipAppVersion(version: string) { + if (this.electronAPIs?.skipAppVersion) { + this.electronAPIs.skipAppVersion(version); + } + } +} + +export default new ElectronUpdateService(); diff --git a/src/services/exportService.ts b/src/services/exportService.ts index 3058858a0..ed3a5cf4f 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -38,7 +38,6 @@ import { } from 'utils/file'; import { updateFileCreationDateInEXIF } from './upload/exifService'; -import { Metadata } from 'types/upload'; import QueueProcessor from './queueProcessor'; import { Collection } from 'types/collection'; import { @@ -244,10 +243,7 @@ class ExportService { file, RecordType.FAILED ); - console.log( - `export failed for fileID:${file.id}, reason:`, - e - ); + logError( e, 'download and save failed for file during export' @@ -459,11 +455,7 @@ class ExportService { await this.exportMotionPhoto(fileStream, file, collectionPath); } else { this.saveMediaFile(collectionPath, fileSaveName, fileStream); - await this.saveMetadataFile( - collectionPath, - fileSaveName, - file.metadata - ); + await this.saveMetadataFile(collectionPath, fileSaveName, file); } } @@ -481,11 +473,7 @@ class ExportService { file.id ); this.saveMediaFile(collectionPath, imageSaveName, imageStream); - await this.saveMetadataFile( - collectionPath, - imageSaveName, - file.metadata - ); + await this.saveMetadataFile(collectionPath, imageSaveName, file); const videoStream = generateStreamFromArrayBuffer(motionPhoto.video); const videoSaveName = getUniqueFileSaveName( @@ -493,20 +481,16 @@ class ExportService { motionPhoto.videoNameTitle, file.id ); - this.saveMediaFile(collectionPath, videoSaveName, videoStream); - await this.saveMetadataFile( - collectionPath, - videoSaveName, - file.metadata - ); + await this.saveMediaFile(collectionPath, videoSaveName, videoStream); + await this.saveMetadataFile(collectionPath, videoSaveName, file); } - private saveMediaFile( + private async saveMediaFile( collectionFolderPath: string, fileSaveName: string, fileStream: ReadableStream ) { - this.electronAPIs.saveStreamToDisk( + await this.electronAPIs.saveStreamToDisk( getFileSavePath(collectionFolderPath, fileSaveName), fileStream ); @@ -514,11 +498,11 @@ class ExportService { private async saveMetadataFile( collectionFolderPath: string, fileSaveName: string, - metadata: Metadata + file: EnteFile ) { await this.electronAPIs.saveFileToDisk( getFileMetadataSavePath(collectionFolderPath, fileSaveName), - getGoogleLikeMetadataFile(fileSaveName, metadata) + getGoogleLikeMetadataFile(fileSaveName, file) ); } @@ -635,7 +619,6 @@ class ExportService { oldFileSavePath, newFileSavePath ); - console.log(oldFileMetadataSavePath, newFileMetadataSavePath); await this.electronAPIs.checkExistsAndRename( oldFileMetadataSavePath, newFileMetadataSavePath diff --git a/src/services/ffmpeg/ffmpegClient.ts b/src/services/ffmpeg/ffmpegClient.ts deleted file mode 100644 index cd4848744..000000000 --- a/src/services/ffmpeg/ffmpegClient.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { createFFmpeg, FFmpeg } from 'ffmpeg-wasm'; -import { getUint8ArrayView } from 'services/readerService'; -import { - parseFFmpegExtractedMetadata, - splitFilenameAndExtension, -} from 'utils/ffmpeg'; - -class FFmpegClient { - private ffmpeg: FFmpeg; - private ready: Promise = null; - constructor() { - this.ffmpeg = createFFmpeg({ - corePath: '/js/ffmpeg/ffmpeg-core.js', - mt: false, - }); - - this.ready = this.init(); - } - - private async init() { - if (!this.ffmpeg.isLoaded()) { - await this.ffmpeg.load(); - } - } - - async generateThumbnail(file: File) { - await this.ready; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, ext] = splitFilenameAndExtension(file.name); - const inputFileName = `${Date.now().toString()}-input.${ext}`; - const thumbFileName = `${Date.now().toString()}-thumb.jpeg`; - this.ffmpeg.FS( - 'writeFile', - inputFileName, - await getUint8ArrayView(file) - ); - let seekTime = 1.0; - let thumb = null; - while (seekTime > 0) { - try { - await this.ffmpeg.run( - '-i', - inputFileName, - '-ss', - `00:00:0${seekTime.toFixed(3)}`, - '-vframes', - '1', - '-vf', - 'scale=-1:720', - thumbFileName - ); - thumb = this.ffmpeg.FS('readFile', thumbFileName); - this.ffmpeg.FS('unlink', thumbFileName); - break; - } catch (e) { - seekTime = Number((seekTime / 10).toFixed(3)); - } - } - this.ffmpeg.FS('unlink', inputFileName); - return thumb; - } - - async extractVideoMetadata(file: File) { - await this.ready; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, ext] = splitFilenameAndExtension(file.name); - const inputFileName = `${Date.now().toString()}-input.${ext}`; - const outFileName = `${Date.now().toString()}-metadata.txt`; - this.ffmpeg.FS( - 'writeFile', - inputFileName, - await getUint8ArrayView(file) - ); - let metadata = null; - - // https://stackoverflow.com/questions/9464617/retrieving-and-saving-media-metadata-using-ffmpeg - // -c [short for codex] copy[(stream_specifier)[ffmpeg.org/ffmpeg.html#Stream-specifiers]] => copies all the stream without re-encoding - // -map_metadata [http://ffmpeg.org/ffmpeg.html#Advanced-options search for map_metadata] => copies all stream metadata to the out - // -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file - await this.ffmpeg.run( - '-i', - inputFileName, - '-c', - 'copy', - '-map_metadata', - '0', - '-f', - 'ffmetadata', - outFileName - ); - metadata = this.ffmpeg.FS('readFile', outFileName); - this.ffmpeg.FS('unlink', outFileName); - this.ffmpeg.FS('unlink', inputFileName); - return parseFFmpegExtractedMetadata(metadata); - } - - async convertToMP4(file: Uint8Array, inputFileName: string) { - await this.ready; - this.ffmpeg.FS('writeFile', inputFileName, file); - await this.ffmpeg.run( - '-i', - inputFileName, - '-preset', - 'ultrafast', - 'output.mp4' - ); - const convertedFile = this.ffmpeg.FS('readFile', 'output.mp4'); - this.ffmpeg.FS('unlink', inputFileName); - this.ffmpeg.FS('unlink', 'output.mp4'); - return convertedFile; - } -} - -export default FFmpegClient; diff --git a/src/services/ffmpeg/ffmpegFactory.ts b/src/services/ffmpeg/ffmpegFactory.ts new file mode 100644 index 000000000..c91032563 --- /dev/null +++ b/src/services/ffmpeg/ffmpegFactory.ts @@ -0,0 +1,28 @@ +import isElectron from 'is-electron'; +import { ElectronFFmpeg } from 'services/electron/ffmpeg'; +import { ElectronFile } from 'types/upload'; +import ComlinkFFmpegWorker from 'utils/comlink/ComlinkFFmpegWorker'; + +export interface IFFmpeg { + run: ( + cmd: string[], + inputFile: File | ElectronFile, + outputFilename: string + ) => Promise; +} + +class FFmpegFactory { + private client: IFFmpeg; + async getFFmpegClient() { + if (!this.client) { + if (isElectron()) { + this.client = new ElectronFFmpeg(); + } else { + this.client = await ComlinkFFmpegWorker.getInstance(); + } + } + return this.client; + } +} + +export default new FFmpegFactory(); diff --git a/src/services/ffmpeg/ffmpegService.ts b/src/services/ffmpeg/ffmpegService.ts index 889eabda1..8464d027b 100644 --- a/src/services/ffmpeg/ffmpegService.ts +++ b/src/services/ffmpeg/ffmpegService.ts @@ -1,93 +1,99 @@ -import { CustomError } from 'utils/error'; +import { + FFMPEG_PLACEHOLDER, + INPUT_PATH_PLACEHOLDER, + OUTPUT_PATH_PLACEHOLDER, +} from 'constants/ffmpeg'; +import { ElectronFile } from 'types/upload'; +import { parseFFmpegExtractedMetadata } from 'utils/ffmpeg'; import { logError } from 'utils/sentry'; -import QueueProcessor from 'services/queueProcessor'; -import { ParsedExtractedMetadata } from 'types/upload'; +import ffmpegFactory from './ffmpegFactory'; -import { FFmpegWorker } from 'utils/comlink'; -import { promiseWithTimeout } from 'utils/common'; - -const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000; - -class FFmpegService { - private ffmpegWorker = null; - private ffmpegTaskQueue = new QueueProcessor(1); - - async init() { - this.ffmpegWorker = await new FFmpegWorker(); - } - - async generateThumbnail(file: File): Promise { - if (!this.ffmpegWorker) { - await this.init(); - } - - const response = this.ffmpegTaskQueue.queueUpRequest(() => - promiseWithTimeout( - this.ffmpegWorker.generateThumbnail(file), - FFMPEG_EXECUTION_WAIT_TIME - ) - ); - try { - return await response.promise; - } catch (e) { - if (e.message === CustomError.REQUEST_CANCELLED) { - // ignore - return null; - } else { - logError(e, 'ffmpeg thumbnail generation failed'); - throw e; - } - } - } - - async extractMetadata(file: File): Promise { - if (!this.ffmpegWorker) { - await this.init(); - } - - const response = this.ffmpegTaskQueue.queueUpRequest(() => - promiseWithTimeout( - this.ffmpegWorker.extractVideoMetadata(file), - FFMPEG_EXECUTION_WAIT_TIME - ) - ); - try { - return await response.promise; - } catch (e) { - if (e.message === CustomError.REQUEST_CANCELLED) { - // ignore - return null; - } else { - logError(e, 'ffmpeg metadata extraction failed'); - throw e; - } - } - } - - async convertToMP4( - file: Uint8Array, - fileName: string - ): Promise { - if (!this.ffmpegWorker) { - await this.init(); - } - - const response = this.ffmpegTaskQueue.queueUpRequest( - async () => await this.ffmpegWorker.convertToMP4(file, fileName) - ); - - try { - return await response.promise; - } catch (e) { - if (e.message === CustomError.REQUEST_CANCELLED) { - // ignore - return null; - } else { - logError(e, 'ffmpeg MP4 conversion failed'); - throw e; +export async function generateVideoThumbnail( + file: File | ElectronFile +): Promise { + try { + let seekTime = 1.0; + const ffmpegClient = await ffmpegFactory.getFFmpegClient(); + while (seekTime > 0) { + try { + return await ffmpegClient.run( + [ + FFMPEG_PLACEHOLDER, + '-i', + INPUT_PATH_PLACEHOLDER, + '-ss', + `00:00:0${seekTime.toFixed(3)}`, + '-vframes', + '1', + '-vf', + 'scale=-1:720', + OUTPUT_PATH_PLACEHOLDER, + ], + file, + 'thumb.jpeg' + ); + } catch (e) { + if (seekTime <= 0) { + throw e; + } } + seekTime = Number((seekTime / 10).toFixed(3)); } + } catch (e) { + logError(e, 'ffmpeg generateVideoThumbnail failed'); + throw e; } } -export default new FFmpegService(); +export async function extractVideoMetadata(file: File | ElectronFile) { + try { + const ffmpegClient = await ffmpegFactory.getFFmpegClient(); + // https://stackoverflow.com/questions/9464617/retrieving-and-saving-media-metadata-using-ffmpeg + // -c [short for codex] copy[(stream_specifier)[ffmpeg.org/ffmpeg.html#Stream-specifiers]] => copies all the stream without re-encoding + // -map_metadata [http://ffmpeg.org/ffmpeg.html#Advanced-options search for map_metadata] => copies all stream metadata to the out + // -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file + const metadata = await ffmpegClient.run( + [ + FFMPEG_PLACEHOLDER, + '-i', + INPUT_PATH_PLACEHOLDER, + '-c', + 'copy', + '-map_metadata', + '0', + '-f', + 'ffmetadata', + OUTPUT_PATH_PLACEHOLDER, + ], + file, + `metadata.txt` + ); + return parseFFmpegExtractedMetadata( + new Uint8Array(await metadata.arrayBuffer()) + ); + } catch (e) { + logError(e, 'ffmpeg extractVideoMetadata failed'); + throw e; + } +} + +export async function convertToMP4(file: File | ElectronFile) { + try { + const ffmpegClient = await ffmpegFactory.getFFmpegClient(); + return await ffmpegClient.run( + [ + FFMPEG_PLACEHOLDER, + '-i', + INPUT_PATH_PLACEHOLDER, + '-preset', + 'ultrafast', + OUTPUT_PATH_PLACEHOLDER, + ], + file, + 'output.mp4' + ); + } catch (e) { + logError(e, 'ffmpeg convertToMP4 failed'); + throw e; + } +} diff --git a/src/services/fileService.ts b/src/services/fileService.ts index 1bafa70be..969b865ad 100644 --- a/src/services/fileService.ts +++ b/src/services/fileService.ts @@ -2,7 +2,6 @@ import { getEndpoint } from 'utils/common/apiUtil'; import localForage from 'utils/storage/localForage'; import { getToken } from 'utils/common/key'; -import { EncryptionResult } from 'types/upload'; import { Collection } from 'types/collection'; import HTTPService from './HTTPService'; import { logError } from 'utils/sentry'; @@ -12,13 +11,15 @@ import { preservePhotoswipeProps, sortFiles, } from 'utils/file'; -import CryptoWorker from 'utils/crypto'; import { eventBus, Events } from './events'; -import { EnteFile, TrashRequest } from 'types/file'; +import { EnteFile, EncryptedEnteFile, TrashRequest } from 'types/file'; import { SetFiles } from 'types/gallery'; import { MAX_TRASH_BATCH_SIZE } from 'constants/file'; import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata'; import { addLogLine } from 'utils/logging'; +import { isCollectionHidden } from 'utils/collection'; +import { CustomError } from 'utils/error'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; const ENDPOINT = getEndpoint(); const FILES_TABLE = 'files'; @@ -69,13 +70,16 @@ export const syncFiles = async ( if (!getToken()) { continue; } + if (isCollectionHidden(collection)) { + throw Error(CustomError.HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED); + } const lastSyncTime = await getCollectionLastSyncTime(collection); if (collection.updationTime === lastSyncTime) { continue; } const fetchedFiles = (await getFiles(collection, lastSyncTime, files, setFiles)) ?? []; - files.push(...fetchedFiles); + files = [...files, ...fetchedFiles]; const latestVersionFiles = new Map(); files.forEach((file) => { const uid = `${file.collectionID}-${file.id}`; @@ -111,7 +115,7 @@ export const getFiles = async ( setFiles: SetFiles ): Promise => { try { - const decryptedFiles: EnteFile[] = []; + let decryptedFiles: EnteFile[] = []; let time = sinceTime; let resp; do { @@ -130,16 +134,18 @@ export const getFiles = async ( } ); - decryptedFiles.push( + decryptedFiles = [ + ...decryptedFiles, ...(await Promise.all( - resp.data.diff.map(async (file: EnteFile) => { + resp.data.diff.map(async (file: EncryptedEnteFile) => { if (!file.isDeleted) { - file = await decryptFile(file, collection.key); + return await decryptFile(file, collection.key); + } else { + return file; } - return file; }) as Promise[] - )) - ); + )), + ]; if (resp.data.diff.length) { time = resp.data.diff.slice(-1)[0].updationTime; @@ -250,16 +256,19 @@ export const updateFileMagicMetadata = async (files: EnteFile[]) => { return; } const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] }; - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); for (const file of files) { - const { file: encryptedMagicMetadata }: EncryptionResult = - await worker.encryptMetadata(file.magicMetadata.data, file.key); + const { file: encryptedMagicMetadata } = + await cryptoWorker.encryptMetadata( + file.magicMetadata.data, + file.key + ); reqBody.metadataList.push({ id: file.id, magicMetadata: { version: file.magicMetadata.version, count: file.magicMetadata.count, - data: encryptedMagicMetadata.encryptedData as unknown as string, + data: encryptedMagicMetadata.encryptedData, header: encryptedMagicMetadata.decryptionHeader, }, }); @@ -284,16 +293,19 @@ export const updateFilePublicMagicMetadata = async (files: EnteFile[]) => { return; } const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] }; - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); for (const file of files) { - const { file: encryptedPubMagicMetadata }: EncryptionResult = - await worker.encryptMetadata(file.pubMagicMetadata.data, file.key); + const { file: encryptedPubMagicMetadata } = + await cryptoWorker.encryptMetadata( + file.pubMagicMetadata.data, + file.key + ); reqBody.metadataList.push({ id: file.id, magicMetadata: { version: file.pubMagicMetadata.version, count: file.pubMagicMetadata.count, - data: encryptedPubMagicMetadata.encryptedData as unknown as string, + data: encryptedPubMagicMetadata.encryptedData, header: encryptedPubMagicMetadata.decryptionHeader, }, }); diff --git a/src/services/heicConversionService.ts b/src/services/heicConversionService.ts new file mode 100644 index 000000000..9a2e39e11 --- /dev/null +++ b/src/services/heicConversionService.ts @@ -0,0 +1,25 @@ +import { logError } from 'utils/sentry'; +import WasmHEICConverterService from './wasmHeicConverter/wasmHEICConverterService'; +import ElectronImageProcessorService from 'services/electron/imageProcessor'; + +class HeicConversionService { + async convert(heicFileData: Blob): Promise { + try { + if (ElectronImageProcessorService.convertAPIExists()) { + try { + return await ElectronImageProcessorService.convertHEIC( + heicFileData + ); + } catch (e) { + return await WasmHEICConverterService.convert(heicFileData); + } + } else { + return await WasmHEICConverterService.convert(heicFileData); + } + } catch (e) { + logError(e, 'failed to convert heic file'); + throw e; + } + } +} +export default new HeicConversionService(); diff --git a/src/services/migrateThumbnailService.ts b/src/services/migrateThumbnailService.ts index 90a86b71d..f8ce6c540 100644 --- a/src/services/migrateThumbnailService.ts +++ b/src/services/migrateThumbnailService.ts @@ -5,14 +5,16 @@ import { getToken } from 'utils/common/key'; import { logError } from 'utils/sentry'; import { getEndpoint } from 'utils/common/apiUtil'; import HTTPService from 'services/HTTPService'; -import CryptoWorker from 'utils/crypto'; import uploadHttpClient from 'services/upload/uploadHttpClient'; import { SetProgressTracker } from 'components/FixLargeThumbnail'; import { getFileType } from 'services/typeDetectionService'; import { getLocalTrash, getTrashedFiles } from './trashService'; -import { EncryptionResult, UploadURL } from 'types/upload'; -import { fileAttribute } from 'types/file'; +import { UploadURL } from 'types/upload'; +import { FileAttributes } from 'types/file'; import { USE_CF_PROXY } from 'constants/upload'; +import { Remote } from 'comlink'; +import { DedicatedCryptoWorker } from 'worker/crypto.worker'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; const ENDPOINT = getEndpoint(); const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB @@ -44,7 +46,7 @@ export async function replaceThumbnail( let completedWithError = false; try { const token = getToken(); - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const files = await getLocalFiles(); const trash = await getLocalTrash(); const trashFiles = getTrashedFiles(trash); @@ -83,7 +85,7 @@ export async function replaceThumbnail( fileTypeInfo ); const newUploadedThumbnail = await uploadThumbnail( - worker, + cryptoWorker, file.key, newThumbnail, uploadURLs.pop() @@ -102,24 +104,26 @@ export async function replaceThumbnail( } export async function uploadThumbnail( - worker, + worker: Remote, fileKey: string, updatedThumbnail: Uint8Array, uploadURL: UploadURL -): Promise { - const { file: encryptedThumbnail }: EncryptionResult = - await worker.encryptThumbnail(updatedThumbnail, fileKey); +): Promise { + const { file: encryptedThumbnail } = await worker.encryptThumbnail( + updatedThumbnail, + fileKey + ); let thumbnailObjectKey: string = null; if (USE_CF_PROXY) { thumbnailObjectKey = await uploadHttpClient.putFileV2( uploadURL, - encryptedThumbnail.encryptedData as Uint8Array, + encryptedThumbnail.encryptedData, () => {} ); } else { thumbnailObjectKey = await uploadHttpClient.putFile( uploadURL, - encryptedThumbnail.encryptedData as Uint8Array, + encryptedThumbnail.encryptedData, () => {} ); } @@ -131,7 +135,7 @@ export async function uploadThumbnail( export async function updateThumbnail( fileID: number, - newThumbnail: fileAttribute + newThumbnail: FileAttributes ) { try { const token = getToken(); diff --git a/src/services/publicCollectionDownloadManager.ts b/src/services/publicCollectionDownloadManager.ts index d649fced6..4fe419688 100644 --- a/src/services/publicCollectionDownloadManager.ts +++ b/src/services/publicCollectionDownloadManager.ts @@ -2,7 +2,6 @@ import { getPublicCollectionFileURL, getPublicCollectionThumbnailURL, } from 'utils/common/apiUtil'; -import CryptoWorker from 'utils/crypto'; import { generateStreamFromArrayBuffer, getRenderableFileURL, @@ -14,11 +13,18 @@ import { EnteFile } from 'types/file'; import { logError } from 'utils/sentry'; import { FILE_TYPE } from 'constants/file'; import { CustomError } from 'utils/error'; +import QueueProcessor from './queueProcessor'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; class PublicCollectionDownloadManager { - private fileObjectURLPromise = new Map>(); + private fileObjectURLPromise = new Map< + string, + Promise<{ original: string[]; converted: string[] }> + >(); private thumbnailObjectURLPromise = new Map>(); + private thumbnailDownloadRequestsProcessor = new QueueProcessor(5); + public async getThumbnail( file: EnteFile, token: string, @@ -46,11 +52,10 @@ class PublicCollectionDownloadManager { if (cacheResp) { return URL.createObjectURL(await cacheResp.blob()); } - const thumb = await this.downloadThumb( - token, - passwordToken, - file - ); + const thumb = + await this.thumbnailDownloadRequestsProcessor.queueUpRequest( + () => this.downloadThumb(token, passwordToken, file) + ).promise; const thumbBlob = new Blob([thumb]); try { await thumbnailCache?.put( @@ -92,10 +97,10 @@ class PublicCollectionDownloadManager { if (typeof resp.data === 'undefined') { throw Error(CustomError.REQUEST_FAILED); } - const worker = await new CryptoWorker(); - const decrypted: Uint8Array = await worker.decryptThumbnail( + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const decrypted = await cryptoWorker.decryptThumbnail( new Uint8Array(resp.data), - await worker.fromB64(file.thumbnail.decryptionHeader), + await cryptoWorker.fromB64(file.thumbnail.decryptionHeader), file.key ); return decrypted; @@ -119,12 +124,11 @@ class PublicCollectionDownloadManager { if (forPreview) { return await getRenderableFileURL(file, fileBlob); } else { - return [ - await createTypedObjectURL( - fileBlob, - file.metadata.title - ), - ]; + const fileURL = await createTypedObjectURL( + fileBlob, + file.metadata.title + ); + return { converted: [fileURL], original: [fileURL] }; } }; if (!this.fileObjectURLPromise.get(fileKey)) { @@ -144,7 +148,7 @@ class PublicCollectionDownloadManager { } async downloadFile(token: string, passwordToken: string, file: EnteFile) { - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); if (!token) { return null; } @@ -166,9 +170,9 @@ class PublicCollectionDownloadManager { if (typeof resp.data === 'undefined') { throw Error(CustomError.REQUEST_FAILED); } - const decrypted: any = await worker.decryptFile( + const decrypted = await cryptoWorker.decryptFile( new Uint8Array(resp.data), - await worker.fromB64(file.file.decryptionHeader), + await cryptoWorker.fromB64(file.file.decryptionHeader), file.key ); return generateStreamFromArrayBuffer(decrypted); @@ -184,12 +188,15 @@ class PublicCollectionDownloadManager { const reader = resp.body.getReader(); const stream = new ReadableStream({ async start(controller) { - const decryptionHeader = await worker.fromB64( + const decryptionHeader = await cryptoWorker.fromB64( file.file.decryptionHeader ); - const fileKey = await worker.fromB64(file.key); + const fileKey = await cryptoWorker.fromB64(file.key); const { pullState, decryptionChunkSize } = - await worker.initDecryption(decryptionHeader, fileKey); + await cryptoWorker.initDecryption( + decryptionHeader, + fileKey + ); let data = new Uint8Array(); // The following function handles each data chunk function push() { @@ -208,7 +215,7 @@ class PublicCollectionDownloadManager { decryptionChunkSize ); const { decryptedData } = - await worker.decryptChunk( + await cryptoWorker.decryptChunk( fileData, pullState ); @@ -221,7 +228,10 @@ class PublicCollectionDownloadManager { } else { if (data) { const { decryptedData } = - await worker.decryptChunk(data, pullState); + await cryptoWorker.decryptChunk( + data, + pullState + ); controller.enqueue(decryptedData); data = null; } diff --git a/src/services/publicCollectionService.ts b/src/services/publicCollectionService.ts index 43554af69..85425b3c8 100644 --- a/src/services/publicCollectionService.ts +++ b/src/services/publicCollectionService.ts @@ -1,18 +1,18 @@ import { getEndpoint } from 'utils/common/apiUtil'; import localForage from 'utils/storage/localForage'; -import { Collection } from 'types/collection'; +import { Collection, EncryptedCollection } from 'types/collection'; import HTTPService from './HTTPService'; import { logError } from 'utils/sentry'; import { decryptFile, mergeMetadata, sortFiles } from 'utils/file'; -import { EnteFile } from 'types/file'; +import { EncryptedEnteFile, EnteFile } from 'types/file'; import { AbuseReportDetails, AbuseReportRequest, LocalSavedPublicCollectionFiles, } from 'types/publicCollection'; -import CryptoWorker from 'utils/crypto'; import { REPORT_REASON } from 'constants/publicCollection'; import { CustomError, parseSharingErrorCodes } from 'utils/error'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; const ENDPOINT = getEndpoint(); const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files'; @@ -20,12 +20,29 @@ const PUBLIC_COLLECTIONS_TABLE = 'public-collections'; export const getPublicCollectionUID = (token: string) => `${token}`; -const getPublicCollectionSyncTimeUID = (collectionUID: string) => +const getPublicCollectionLastSyncTimeKey = (collectionUID: string) => `public-${collectionUID}-time`; const getPublicCollectionPasswordKey = (collectionUID: string) => `public-${collectionUID}-passkey`; +const getPublicCollectionUploaderNameKey = (collectionUID: string) => + `public-${collectionUID}-uploaderName`; + +export const getPublicCollectionUploaderName = async (collectionUID: string) => + await localForage.getItem( + getPublicCollectionUploaderNameKey(collectionUID) + ); + +export const savePublicCollectionUploaderName = async ( + collectionUID: string, + uploaderName: string +) => + await localForage.setItem( + getPublicCollectionUploaderNameKey(collectionUID), + uploaderName + ); + export const getLocalPublicFiles = async (collectionUID: string) => { const localSavedPublicCollectionFiles = ( @@ -129,15 +146,15 @@ const dedupeCollectionFiles = ( const getPublicCollectionLastSyncTime = async (collectionUID: string) => (await localForage.getItem( - getPublicCollectionSyncTimeUID(collectionUID) + getPublicCollectionLastSyncTimeKey(collectionUID) )) ?? 0; -const setPublicCollectionLastSyncTime = async ( +const savePublicCollectionLastSyncTime = async ( collectionUID: string, time: number ) => await localForage.setItem( - getPublicCollectionSyncTimeUID(collectionUID), + getPublicCollectionLastSyncTimeKey(collectionUID), time ); @@ -151,7 +168,7 @@ export const syncPublicFiles = async ( let files: EnteFile[] = []; const collectionUID = getPublicCollectionUID(token); const localFiles = await getLocalPublicFiles(collectionUID); - files.push(...localFiles); + files = [...files, ...localFiles]; try { if (!token) { return files; @@ -171,7 +188,7 @@ export const syncPublicFiles = async ( setPublicFiles ); - files.push(...fetchedFiles); + files = [...files, ...fetchedFiles]; const latestVersionFiles = new Map(); files.forEach((file) => { const uid = `${file.collectionID}-${file.id}`; @@ -191,7 +208,7 @@ export const syncPublicFiles = async ( files.push(file); } await savePublicCollectionFiles(collectionUID, files); - await setPublicCollectionLastSyncTime( + await savePublicCollectionLastSyncTime( collectionUID, collection.updationTime ); @@ -200,7 +217,6 @@ export const syncPublicFiles = async ( const parsedError = parseSharingErrorCodes(e); logError(e, 'failed to sync shared collection files'); if (parsedError.message === CustomError.TOKEN_EXPIRED) { - console.log('invalid token or password'); throw e; } } @@ -220,7 +236,7 @@ const getPublicFiles = async ( setPublicFiles: (files: EnteFile[]) => void ): Promise => { try { - const decryptedFiles: EnteFile[] = []; + let decryptedFiles: EnteFile[] = []; let time = sinceTime; let resp; do { @@ -240,16 +256,18 @@ const getPublicFiles = async ( }), } ); - decryptedFiles.push( + decryptedFiles = [ + ...decryptedFiles, ...(await Promise.all( - resp.data.diff.map(async (file: EnteFile) => { + resp.data.diff.map(async (file: EncryptedEnteFile) => { if (!file.isDeleted) { - file = await decryptFile(file, collection.key); + return await decryptFile(file, collection.key); + } else { + return file; } - return file; }) as Promise[] - )) - ); + )), + ]; if (resp.data.diff.length) { time = resp.data.diff.slice(-1)[0].updationTime; @@ -322,14 +340,14 @@ export const verifyPublicCollectionPassword = async ( }; const decryptCollectionName = async ( - collection: Collection, + collection: EncryptedCollection, collectionKey: string ) => { - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); return (collection.name = collection.name || - (await worker.decryptToUTF8( + (await cryptoWorker.decryptToUTF8( collection.encryptedName, collection.nameDecryptionNonce, collectionKey @@ -378,7 +396,9 @@ export const removePublicCollectionWithFiles = async ( export const removePublicFiles = async (collectionUID: string) => { await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID)); - await localForage.removeItem(getPublicCollectionSyncTimeUID(collectionUID)); + await localForage.removeItem( + getPublicCollectionLastSyncTimeKey(collectionUID) + ); const publicCollectionFiles = (await localForage.getItem( diff --git a/src/services/queueProcessor.ts b/src/services/queueProcessor.ts index 5f5e939c6..ad63d3227 100644 --- a/src/services/queueProcessor.ts +++ b/src/services/queueProcessor.ts @@ -8,6 +8,11 @@ interface RequestQueueItem { canceller: { exec: () => void }; } +export enum PROCESSING_STRATEGY { + FIFO, + LIFO, +} + export interface RequestCanceller { exec: () => void; } @@ -17,7 +22,10 @@ export default class QueueProcessor { private requestInProcessing = 0; - constructor(private maxParallelProcesses: number) {} + constructor( + private maxParallelProcesses: number, + private processingStrategy = PROCESSING_STRATEGY.FIFO + ) {} public queueUpRequest( request: (canceller?: RequestCanceller) => Promise @@ -52,7 +60,10 @@ export default class QueueProcessor { private async processQueue() { while (this.requestQueue.length > 0) { - const queueItem = this.requestQueue.shift(); + const queueItem = + this.processingStrategy === PROCESSING_STRATEGY.LIFO + ? this.requestQueue.pop() + : this.requestQueue.shift(); let response = null; if (queueItem.isCanceled.status) { diff --git a/src/services/searchService.ts b/src/services/searchService.ts index 0d6aa3ffa..dab462df7 100644 --- a/src/services/searchService.ts +++ b/src/services/searchService.ts @@ -24,6 +24,7 @@ import textService from './machineLearning/textService'; import { FILE_TYPE } from 'constants/file'; import { getFormattedDate, isInsideBox, isSameDayAnyYear } from 'utils/search'; import { Person, ThingClass } from 'types/machineLearning'; +import { getUniqueFiles } from 'utils/file'; const ENDPOINT = getEndpoint(); @@ -174,7 +175,7 @@ export function searchCollection( } function searchFiles(searchPhrase: string, files: EnteFile[]) { - return files + return getUniqueFiles(files) .map((file) => ({ title: file.metadata.title, id: file.id, @@ -225,6 +226,7 @@ function getFileSuggestion( })); } +// eslint-disable-next-line @typescript-eslint/no-unused-vars async function getLocationSuggestions(searchPhrase: string) { const locationResults = await searchLocation(searchPhrase); diff --git a/src/services/trashService.ts b/src/services/trashService.ts index 4daf3b5f1..0d601bb17 100644 --- a/src/services/trashService.ts +++ b/src/services/trashService.ts @@ -14,7 +14,7 @@ import { getCollection } from './collectionService'; import { EnteFile } from 'types/file'; import HTTPService from './HTTPService'; -import { Trash, TrashItem } from 'types/trash'; +import { EncryptedTrashItem, Trash } from 'types/trash'; const TRASH = 'file-trash'; const TRASH_TIME = 'trash-time'; @@ -99,7 +99,8 @@ export const updateTrash = async ( 'X-Auth-Token': token, } ); - for (const trashItem of resp.data.diff as TrashItem[]) { + // #Perf: This can be optimized by running the decryption in parallel + for (const trashItem of resp.data.diff as EncryptedTrashItem[]) { const collectionID = trashItem.file.collectionID; let collection = collections.get(collectionID); if (!collection) { @@ -110,19 +111,21 @@ export const updateTrash = async ( ]); } if (!trashItem.isDeleted && !trashItem.isRestored) { - trashItem.file = await decryptFile( + const decryptedFile = await decryptFile( trashItem.file, collection.key ); + updatedTrash.push({ ...trashItem, file: decryptedFile }); + } else { + updatedTrash = updatedTrash.filter( + (item) => item.file.id !== trashItem.file.id + ); } - updatedTrash.push(trashItem); } if (resp.data.diff.length) { time = resp.data.diff.slice(-1)[0].updatedAt; } - updatedTrash = removeDuplicates(updatedTrash); - updatedTrash = removeRestoredOrDeletedTrashItems(updatedTrash); setFiles( preservePhotoswipeProps( @@ -145,28 +148,6 @@ export const updateTrash = async ( return currentTrash; }; -function removeDuplicates(trash: Trash) { - const latestVersionTrashItems = new Map(); - trash.forEach(({ file, updatedAt, ...rest }) => { - if ( - !latestVersionTrashItems.has(file.id) || - latestVersionTrashItems.get(file.id).updatedAt < updatedAt - ) { - latestVersionTrashItems.set(file.id, { file, updatedAt, ...rest }); - } - }); - trash = []; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [_, trashedFile] of latestVersionTrashItems) { - trash.push(trashedFile); - } - return trash; -} - -function removeRestoredOrDeletedTrashItems(trash: Trash) { - return trash.filter((item) => !item.isDeleted && !item.isRestored); -} - export function getTrashedFiles(trash: Trash) { return mergeMetadata( trash.map((trashedFile) => ({ diff --git a/src/services/typeDetectionService.ts b/src/services/typeDetectionService.ts index 7d25b82c4..97ccd53e5 100644 --- a/src/services/typeDetectionService.ts +++ b/src/services/typeDetectionService.ts @@ -73,6 +73,7 @@ async function extractElectronFileType(file: ElectronFile) { const stream = await file.stream(); const reader = stream.getReader(); const { value: fileDataChunk } = await reader.read(); + await reader.cancel(); return getFileTypeFromBuffer(fileDataChunk); } diff --git a/src/services/updateCreationTimeWithExif.ts b/src/services/updateCreationTimeWithExif.ts index fc6dd54fc..6d17a3674 100644 --- a/src/services/updateCreationTimeWithExif.ts +++ b/src/services/updateCreationTimeWithExif.ts @@ -36,7 +36,8 @@ export async function updateCreationTimeWithExif( if (file.metadata.fileType !== FILE_TYPE.IMAGE) { continue; } - const fileURL = await downloadManager.getFile(file)[0]; + const fileURL = (await downloadManager.getFile(file)) + .original[0]; const fileObject = await getFileFromURL(fileURL); const fileTypeInfo = await getFileType(fileObject); const exifData = await getRawExif(fileObject, fileTypeInfo); diff --git a/src/services/upload/encryptionService.ts b/src/services/upload/encryptionService.ts index edbc4864b..0779b958c 100644 --- a/src/services/upload/encryptionService.ts +++ b/src/services/upload/encryptionService.ts @@ -1,4 +1,5 @@ -import { DataStream, EncryptionResult, isDataStream } from 'types/upload'; +import { EncryptionResult } from 'types/crypto'; +import { DataStream, isDataStream } from 'types/upload'; async function encryptFileStream(worker, fileData: DataStream) { const { stream, chunkCount } = fileData; @@ -33,7 +34,7 @@ async function encryptFileStream(worker, fileData: DataStream) { export async function encryptFiledata( worker, filedata: Uint8Array | DataStream -): Promise { +): Promise> { return isDataStream(filedata) ? await encryptFileStream(worker, filedata) : await worker.encryptFile(filedata); diff --git a/src/services/upload/exifService.ts b/src/services/upload/exifService.ts index ee6a3c906..9cbb11fac 100644 --- a/src/services/upload/exifService.ts +++ b/src/services/upload/exifService.ts @@ -62,8 +62,7 @@ export async function updateFileCreationDateInEXIF( updatedDate: Date ) { try { - const fileURL = URL.createObjectURL(fileBlob); - let imageDataURL = await convertImageToDataURL(reader, fileURL); + let imageDataURL = await convertImageToDataURL(reader, fileBlob); imageDataURL = 'data:image/jpeg;base64' + imageDataURL.slice(imageDataURL.indexOf(',')); @@ -83,8 +82,7 @@ export async function updateFileCreationDateInEXIF( } } -async function convertImageToDataURL(reader: FileReader, url: string) { - const blob = await fetch(url).then((r) => r.blob()); +async function convertImageToDataURL(reader: FileReader, blob: Blob) { const dataURL = await new Promise((resolve) => { reader.onload = () => resolve(reader.result as string); reader.readAsDataURL(blob); diff --git a/src/services/upload/fileService.ts b/src/services/upload/fileService.ts index 939e769e6..965fcadcb 100644 --- a/src/services/upload/fileService.ts +++ b/src/services/upload/fileService.ts @@ -3,9 +3,7 @@ import { FileTypeInfo, FileInMemory, Metadata, - B64EncryptionResult, EncryptedFile, - EncryptionResult, FileWithMetadata, ParsedMetadataJSONMap, DataStream, @@ -22,6 +20,9 @@ import { getUint8ArrayView, } from '../readerService'; import { generateThumbnail } from './thumbnailService'; +import { DedicatedCryptoWorker } from 'worker/crypto.worker'; +import { Remote } from 'comlink'; +import { EncryptedMagicMetadata } from 'types/magicMetadata'; const EDITED_FILE_SUFFIX = '-edited'; @@ -68,10 +69,11 @@ export async function readFile( } export async function extractFileMetadata( + worker, parsedMetadataJSONMap: ParsedMetadataJSONMap, - rawFile: File | ElectronFile, collectionID: number, - fileTypeInfo: FileTypeInfo + fileTypeInfo: FileTypeInfo, + rawFile: File | ElectronFile ) { const originalName = getFileOriginalName(rawFile); const googleMetadata = @@ -79,6 +81,7 @@ export async function extractFileMetadata( getMetadataJSONMapKey(collectionID, originalName) ) ?? {}; const extractedMetadata: Metadata = await extractMetadata( + worker, rawFile, fileTypeInfo ); @@ -93,7 +96,7 @@ export async function extractFileMetadata( } export async function encryptFile( - worker: any, + worker: Remote, file: FileWithMetadata, encryptionKey: string ): Promise { @@ -103,21 +106,38 @@ export async function encryptFile( file.filedata ); - const { file: encryptedThumbnail }: EncryptionResult = - await worker.encryptThumbnail(file.thumbnail, fileKey); - const { file: encryptedMetadata }: EncryptionResult = - await worker.encryptMetadata(file.metadata, fileKey); - - const encryptedKey: B64EncryptionResult = await worker.encryptToB64( - fileKey, - encryptionKey + const { file: encryptedThumbnail } = await worker.encryptThumbnail( + file.thumbnail, + fileKey ); + const { file: encryptedMetadata } = await worker.encryptMetadata( + file.metadata, + fileKey + ); + + let encryptedPubMagicMetadata: EncryptedMagicMetadata; + if (file.pubMagicMetadata) { + const { file: encryptedPubMagicMetadataData } = + await worker.encryptMetadata( + file.pubMagicMetadata.data, + fileKey + ); + encryptedPubMagicMetadata = { + version: file.pubMagicMetadata.version, + count: file.pubMagicMetadata.count, + data: encryptedPubMagicMetadataData.encryptedData, + header: encryptedPubMagicMetadataData.decryptionHeader, + }; + } + + const encryptedKey = await worker.encryptToB64(fileKey, encryptionKey); const result: EncryptedFile = { file: { file: encryptedFiledata, thumbnail: encryptedThumbnail, metadata: encryptedMetadata, + pubMagicMetadata: encryptedPubMagicMetadata, localID: file.localID, }, fileKey: encryptedKey, diff --git a/src/services/upload/hashService.tsx b/src/services/upload/hashService.tsx index fb8dfb0a5..e0a4766c0 100644 --- a/src/services/upload/hashService.tsx +++ b/src/services/upload/hashService.tsx @@ -1,11 +1,13 @@ import { FILE_READER_CHUNK_SIZE } from 'constants/upload'; import { getFileStream, getElectronFileStream } from 'services/readerService'; import { ElectronFile, DataStream } from 'types/upload'; -import CryptoWorker from 'utils/crypto'; +import { CustomError } from 'utils/error'; +import { addLogLine, getFileNameSize } from 'utils/logging'; import { logError } from 'utils/sentry'; -export async function getFileHash(file: File | ElectronFile) { +export async function getFileHash(worker, file: File | ElectronFile) { try { + addLogLine(`getFileHash called for ${getFileNameSize(file)}`); let filedata: DataStream; if (file instanceof File) { filedata = getFileStream(file, FILE_READER_CHUNK_SIZE); @@ -15,22 +17,29 @@ export async function getFileHash(file: File | ElectronFile) { FILE_READER_CHUNK_SIZE ); } - const cryptoWorker = await new CryptoWorker(); - const hashState = await cryptoWorker.initChunkHashing(); + const hashState = await worker.initChunkHashing(); - const reader = filedata.stream.getReader(); - // eslint-disable-next-line no-constant-condition - while (true) { - const { done, value: chunk } = await reader.read(); + const streamReader = filedata.stream.getReader(); + for (let i = 0; i < filedata.chunkCount; i++) { + const { done, value: chunk } = await streamReader.read(); if (done) { - break; + throw Error(CustomError.CHUNK_LESS_THAN_EXPECTED); } - await cryptoWorker.hashFileChunk(hashState, Uint8Array.from(chunk)); + await worker.hashFileChunk(hashState, Uint8Array.from(chunk)); } - const hash = await cryptoWorker.completeChunkHashing(hashState); + const { done } = await streamReader.read(); + if (!done) { + throw Error(CustomError.CHUNK_MORE_THAN_EXPECTED); + } + const hash = await worker.completeChunkHashing(hashState); + addLogLine( + `file hashing completed successfully ${getFileNameSize(file)}` + ); return hash; } catch (e) { logError(e, 'getFileHash failed'); - throw e; + addLogLine( + `file hashing failed ${getFileNameSize(file)} ,${e.message} ` + ); } } diff --git a/src/services/upload/livePhotoService.ts b/src/services/upload/livePhotoService.ts index 1b2000cc1..f530692cf 100644 --- a/src/services/upload/livePhotoService.ts +++ b/src/services/upload/livePhotoService.ts @@ -1,20 +1,23 @@ import { FILE_TYPE } from 'constants/file'; import { LIVE_PHOTO_ASSET_SIZE_LIMIT } from 'constants/upload'; import { encodeMotionPhoto } from 'services/motionPhotoService'; +import { getFileType } from 'services/typeDetectionService'; import { ElectronFile, FileTypeInfo, FileWithCollection, LivePhotoAssets, - Metadata, + ParsedMetadataJSONMap, } from 'types/upload'; import { CustomError } from 'utils/error'; -import { isImageOrVideo, splitFilenameAndExtension } from 'utils/file'; +import { getFileTypeFromExtensionForLivePhotoClustering } from 'utils/file/livePhoto'; +import { splitFilenameAndExtension, isImageOrVideo } from 'utils/file'; import { logError } from 'utils/sentry'; import { getUint8ArrayView } from '../readerService'; +import { extractFileMetadata } from './fileService'; +import { getFileHash } from './hashService'; import { generateThumbnail } from './thumbnailService'; -import uploadService from './uploadService'; -import UploadService from './uploadService'; +import uploadCancelService from './uploadCancelService'; interface LivePhotoIdentifier { collectionID: number; @@ -23,56 +26,58 @@ interface LivePhotoIdentifier { size: number; } -interface Asset { - file: File | ElectronFile; - metadata: Metadata; - fileTypeInfo: FileTypeInfo; -} - -const ENTE_LIVE_PHOTO_FORMAT = 'elp'; - const UNDERSCORE_THREE = '_3'; const UNDERSCORE = '_'; -export function getLivePhotoFileType( - imageFileTypeInfo: FileTypeInfo, - videoTypeInfo: FileTypeInfo -): FileTypeInfo { +export async function getLivePhotoFileType( + livePhotoAssets: LivePhotoAssets +): Promise { + const imageFileTypeInfo = await getFileType(livePhotoAssets.image); + const videoFileTypeInfo = await getFileType(livePhotoAssets.video); return { fileType: FILE_TYPE.LIVE_PHOTO, - exactType: `${imageFileTypeInfo.exactType}+${videoTypeInfo.exactType}`, + exactType: `${imageFileTypeInfo.exactType}+${videoFileTypeInfo.exactType}`, imageType: imageFileTypeInfo.exactType, - videoType: videoTypeInfo.exactType, + videoType: videoFileTypeInfo.exactType, }; } -export function getLivePhotoMetadata( - imageMetadata: Metadata, - videoMetadata: Metadata +export async function extractLivePhotoMetadata( + worker, + parsedMetadataJSONMap: ParsedMetadataJSONMap, + collectionID: number, + fileTypeInfo: FileTypeInfo, + livePhotoAssets: LivePhotoAssets ) { + const imageFileTypeInfo: FileTypeInfo = { + fileType: FILE_TYPE.IMAGE, + exactType: fileTypeInfo.imageType, + }; + const imageMetadata = await extractFileMetadata( + worker, + parsedMetadataJSONMap, + collectionID, + imageFileTypeInfo, + livePhotoAssets.image + ); + const videoHash = await getFileHash(worker, livePhotoAssets.video); return { ...imageMetadata, - title: getLivePhotoName(imageMetadata.title), + title: getLivePhotoName(livePhotoAssets), fileType: FILE_TYPE.LIVE_PHOTO, imageHash: imageMetadata.hash, - videoHash: videoMetadata.hash, + videoHash: videoHash, hash: undefined, }; } -export function getLivePhotoFilePath(imageAsset: Asset): string { - return getLivePhotoName((imageAsset.file as any).path); -} - export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) { return livePhotoAssets.image.size + livePhotoAssets.video.size; } -export function getLivePhotoName(imageTitle: string) { - return `${ - splitFilenameAndExtension(imageTitle)[0] - }.${ENTE_LIVE_PHOTO_FORMAT}`; +export function getLivePhotoName(livePhotoAssets: LivePhotoAssets) { + return livePhotoAssets.image.name; } export async function readLivePhoto( @@ -103,7 +108,7 @@ export async function readLivePhoto( }; } -export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) { +export async function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) { try { const analysedMediaFiles: FileWithCollection[] = []; mediaFiles @@ -120,59 +125,48 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) { ); let index = 0; while (index < mediaFiles.length - 1) { + if (uploadCancelService.isUploadCancelationRequested()) { + throw Error(CustomError.UPLOAD_CANCELLED); + } const firstMediaFile = mediaFiles[index]; const secondMediaFile = mediaFiles[index + 1]; - const { - fileTypeInfo: firstFileTypeInfo, - metadata: firstFileMetadata, - } = UploadService.getFileMetadataAndFileTypeInfo( - firstMediaFile.localID - ); - const { - fileTypeInfo: secondFileFileInfo, - metadata: secondFileMetadata, - } = UploadService.getFileMetadataAndFileTypeInfo( - secondMediaFile.localID - ); + const firstFileType = + getFileTypeFromExtensionForLivePhotoClustering( + firstMediaFile.file.name + ); + const secondFileType = + getFileTypeFromExtensionForLivePhotoClustering( + secondMediaFile.file.name + ); const firstFileIdentifier: LivePhotoIdentifier = { collectionID: firstMediaFile.collectionID, - fileType: firstFileTypeInfo.fileType, + fileType: firstFileType, name: firstMediaFile.file.name, size: firstMediaFile.file.size, }; const secondFileIdentifier: LivePhotoIdentifier = { collectionID: secondMediaFile.collectionID, - fileType: secondFileFileInfo.fileType, + fileType: secondFileType, name: secondMediaFile.file.name, size: secondMediaFile.file.size, }; - const firstAsset = { - file: firstMediaFile.file, - metadata: firstFileMetadata, - fileTypeInfo: firstFileTypeInfo, - }; - const secondAsset = { - file: secondMediaFile.file, - metadata: secondFileMetadata, - fileTypeInfo: secondFileFileInfo, - }; if ( areFilesLivePhotoAssets( firstFileIdentifier, secondFileIdentifier ) ) { - let imageAsset: Asset; - let videoAsset: Asset; + let imageFile: File | ElectronFile; + let videoFile: File | ElectronFile; if ( - firstFileTypeInfo.fileType === FILE_TYPE.IMAGE && - secondFileFileInfo.fileType === FILE_TYPE.VIDEO + firstFileType === FILE_TYPE.IMAGE && + secondFileType === FILE_TYPE.VIDEO ) { - imageAsset = firstAsset; - videoAsset = secondAsset; + imageFile = firstMediaFile.file; + videoFile = secondMediaFile.file; } else { - videoAsset = firstAsset; - imageAsset = secondAsset; + videoFile = firstMediaFile.file; + imageFile = secondMediaFile.file; } const livePhotoLocalID = firstMediaFile.localID; analysedMediaFiles.push({ @@ -180,25 +174,10 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) { collectionID: firstMediaFile.collectionID, isLivePhoto: true, livePhotoAssets: { - image: imageAsset.file, - video: videoAsset.file, + image: imageFile, + video: videoFile, }, }); - const livePhotoFileTypeInfo: FileTypeInfo = - getLivePhotoFileType( - imageAsset.fileTypeInfo, - videoAsset.fileTypeInfo - ); - const livePhotoMetadata: Metadata = getLivePhotoMetadata( - imageAsset.metadata, - videoAsset.metadata - ); - const livePhotoPath = getLivePhotoFilePath(imageAsset); - uploadService.setFileMetadataAndFileTypeInfo(livePhotoLocalID, { - fileTypeInfo: { ...livePhotoFileTypeInfo }, - metadata: { ...livePhotoMetadata }, - filePath: livePhotoPath, - }); index += 2; } else { analysedMediaFiles.push({ @@ -216,8 +195,12 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) { } return analysedMediaFiles; } catch (e) { - logError(e, 'failed to cluster live photo'); - throw e; + if (e.message === CustomError.UPLOAD_CANCELLED) { + throw e; + } else { + logError(e, 'failed to cluster live photo'); + throw e; + } } } diff --git a/src/services/upload/magicMetadataService.ts b/src/services/upload/magicMetadataService.ts new file mode 100644 index 000000000..cb286b281 --- /dev/null +++ b/src/services/upload/magicMetadataService.ts @@ -0,0 +1,17 @@ +import { + NEW_FILE_MAGIC_METADATA, + FilePublicMagicMetadataProps, + FilePublicMagicMetadata, +} from 'types/magicMetadata'; +import { updateMagicMetadataProps } from 'utils/magicMetadata'; + +export async function constructPublicMagicMetadata( + publicMagicMetadataProps: FilePublicMagicMetadataProps +): Promise { + const pubMagicMetadata = await updateMagicMetadataProps( + NEW_FILE_MAGIC_METADATA, + null, + publicMagicMetadataProps + ); + return pubMagicMetadata; +} diff --git a/src/services/upload/metadataService.ts b/src/services/upload/metadataService.ts index a6cf979ae..a49a383db 100644 --- a/src/services/upload/metadataService.ts +++ b/src/services/upload/metadataService.ts @@ -30,6 +30,7 @@ const NULL_PARSED_METADATA_JSON: ParsedMetadataJSON = { }; export async function extractMetadata( + worker, receivedFile: File | ElectronFile, fileTypeInfo: FileTypeInfo ) { @@ -39,7 +40,7 @@ export async function extractMetadata( } else if (fileTypeInfo.fileType === FILE_TYPE.VIDEO) { extractedMetadata = await getVideoMetadata(receivedFile); } - const fileHash = await getFileHash(receivedFile); + const fileHash = await getFileHash(worker, receivedFile); const metadata: Metadata = { title: receivedFile.name, diff --git a/src/services/upload/publicUploadHttpClient.ts b/src/services/upload/publicUploadHttpClient.ts new file mode 100644 index 000000000..225a329d1 --- /dev/null +++ b/src/services/upload/publicUploadHttpClient.ts @@ -0,0 +1,116 @@ +import HTTPService from 'services/HTTPService'; +import { getEndpoint } from 'utils/common/apiUtil'; +import { logError } from 'utils/sentry'; +import { EnteFile } from 'types/file'; +import { CustomError, handleUploadError } from 'utils/error'; +import { UploadFile, UploadURL, MultipartUploadURLs } from 'types/upload'; +import { retryHTTPCall } from 'utils/upload/uploadRetrier'; + +const ENDPOINT = getEndpoint(); + +const MAX_URL_REQUESTS = 50; + +class PublicUploadHttpClient { + private uploadURLFetchInProgress = null; + + async uploadFile( + uploadFile: UploadFile, + token: string, + passwordToken: string + ): Promise { + try { + if (!token) { + throw Error(CustomError.TOKEN_MISSING); + } + const response = await retryHTTPCall( + () => + HTTPService.post( + `${ENDPOINT}/public-collection/file`, + uploadFile, + null, + { + 'X-Auth-Access-Token': token, + ...(passwordToken && { + 'X-Auth-Access-Token-JWT': passwordToken, + }), + } + ), + handleUploadError + ); + return response.data; + } catch (e) { + logError(e, 'upload public File Failed'); + throw e; + } + } + + async fetchUploadURLs( + count: number, + urlStore: UploadURL[], + token: string, + passwordToken: string + ): Promise { + try { + if (!this.uploadURLFetchInProgress) { + try { + if (!token) { + throw Error(CustomError.TOKEN_MISSING); + } + this.uploadURLFetchInProgress = HTTPService.get( + `${ENDPOINT}/public-collection/upload-urls`, + { + count: Math.min(MAX_URL_REQUESTS, count * 2), + }, + { + 'X-Auth-Access-Token': token, + ...(passwordToken && { + 'X-Auth-Access-Token-JWT': passwordToken, + }), + } + ); + const response = await this.uploadURLFetchInProgress; + for (const url of response.data['urls']) { + urlStore.push(url); + } + } finally { + this.uploadURLFetchInProgress = null; + } + } + return this.uploadURLFetchInProgress; + } catch (e) { + logError(e, 'fetch public upload-url failed '); + throw e; + } + } + + async fetchMultipartUploadURLs( + count: number, + token: string, + passwordToken: string + ): Promise { + try { + if (!token) { + throw Error(CustomError.TOKEN_MISSING); + } + const response = await HTTPService.get( + `${ENDPOINT}/public-collection/multipart-upload-urls`, + { + count, + }, + { + 'X-Auth-Access-Token': token, + ...(passwordToken && { + 'X-Auth-Access-Token-JWT': passwordToken, + }), + } + ); + + return response.data['urls']; + } catch (e) { + logError(e, 'fetch public multipart-upload-url failed'); + throw e; + } + } +} + +export default new PublicUploadHttpClient(); diff --git a/src/services/upload/thumbnailService.ts b/src/services/upload/thumbnailService.ts index d5b13046a..74215c1ee 100644 --- a/src/services/upload/thumbnailService.ts +++ b/src/services/upload/thumbnailService.ts @@ -2,13 +2,14 @@ import { FILE_TYPE } from 'constants/file'; import { CustomError, errorWithContext } from 'utils/error'; import { logError } from 'utils/sentry'; import { BLACK_THUMBNAIL_BASE64 } from 'constants/upload'; -import FFmpegService from 'services/ffmpeg/ffmpegService'; +import * as FFmpegService from 'services/ffmpeg/ffmpegService'; +import ElectronImageProcessorService from 'services/electron/imageProcessor'; import { convertBytesToHumanReadable } from 'utils/file/size'; import { isExactTypeHEIC } from 'utils/file'; import { ElectronFile, FileTypeInfo } from 'types/upload'; import { getUint8ArrayView } from '../readerService'; -import HEICConverter from 'services/heicConverter/heicConverterService'; import { getFileNameSize, addLogLine } from 'utils/logging'; +import HeicConversionService from 'services/heicConversionService'; const MAX_THUMBNAIL_DIMENSION = 720; const MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF = 10; @@ -30,48 +31,24 @@ export async function generateThumbnail( try { addLogLine(`generating thumbnail for ${getFileNameSize(file)}`); let hasStaticThumbnail = false; - let canvas = document.createElement('canvas'); let thumbnail: Uint8Array; try { - if (!(file instanceof File)) { - file = new File([await file.blob()], file.name); - } if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { - const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType); - canvas = await generateImageThumbnail(file, isHEIC); + thumbnail = await generateImageThumbnail(file, fileTypeInfo); } else { - try { - addLogLine( - `ffmpeg generateThumbnail called for ${getFileNameSize( - file - )}` - ); - - const thumb = await FFmpegService.generateThumbnail(file); - addLogLine( - `ffmpeg thumbnail successfully generated ${getFileNameSize( - file - )}` - ); - const dummyImageFile = new File([thumb], file.name); - canvas = await generateImageThumbnail( - dummyImageFile, - false - ); - } catch (e) { - addLogLine( - `ffmpeg thumbnail generated failed ${getFileNameSize( - file - )} error: ${e.message}` - ); - logError(e, 'failed to generate thumbnail using ffmpeg', { - fileFormat: fileTypeInfo.exactType, - }); - canvas = await generateVideoThumbnail(file); - } + thumbnail = await generateVideoThumbnail(file, fileTypeInfo); + } + if (thumbnail.length > 1.5 * MAX_THUMBNAIL_SIZE) { + logError( + Error('thumbnail_too_large'), + 'thumbnail greater than max limit', + { + thumbnailSize: convertBytesToHumanReadable( + thumbnail.length + ), + } + ); } - const thumbnailBlob = await thumbnailCanvasToBlob(canvas); - thumbnail = await getUint8ArrayView(thumbnailBlob); if (thumbnail.length === 0) { throw Error('EMPTY THUMBNAIL'); } @@ -99,24 +76,57 @@ export async function generateThumbnail( } } -export async function generateImageThumbnail(file: File, isHEIC: boolean) { +async function generateImageThumbnail( + file: File | ElectronFile, + fileTypeInfo: FileTypeInfo +) { + if (ElectronImageProcessorService.generateImageThumbnailAPIExists()) { + try { + return await ElectronImageProcessorService.generateImageThumbnail( + file, + MAX_THUMBNAIL_DIMENSION, + MAX_THUMBNAIL_SIZE + ); + } catch (e) { + logError( + e, + 'Error generating thumbnail using electron image processor', + { + fileFormat: fileTypeInfo.exactType, + } + ); + return await generateImageThumbnailUsingCanvas(file, fileTypeInfo); + } + } else { + return await generateImageThumbnailUsingCanvas(file, fileTypeInfo); + } +} + +export async function generateImageThumbnailUsingCanvas( + file: File | ElectronFile, + fileTypeInfo: FileTypeInfo +) { const canvas = document.createElement('canvas'); const canvasCTX = canvas.getContext('2d'); let imageURL = null; let timeout = null; - + const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType); if (isHEIC) { addLogLine(`HEICConverter called for ${getFileNameSize(file)}`); - file = new File([await HEICConverter.convert(file)], file.name); + const convertedBlob = await HeicConversionService.convert( + new Blob([await file.arrayBuffer()]) + ); + file = new File([convertedBlob], file.name); addLogLine(`${getFileNameSize(file)} successfully converted`); } let image = new Image(); - imageURL = URL.createObjectURL(file); - image.setAttribute('src', imageURL); + imageURL = URL.createObjectURL(new Blob([await file.arrayBuffer()])); await new Promise((resolve, reject) => { + image.setAttribute('src', imageURL); image.onload = () => { try { + URL.revokeObjectURL(imageURL); const imageDimension = { width: image.width, height: image.height, @@ -150,21 +160,56 @@ export async function generateImageThumbnail(file: File, isHEIC: boolean) { WAIT_TIME_THUMBNAIL_GENERATION ); }); - return canvas; + const thumbnailBlob = await getCompressedThumbnailBlobFromCanvas(canvas); + return await getUint8ArrayView(thumbnailBlob); } -export async function generateVideoThumbnail(file: File) { +async function generateVideoThumbnail( + file: File | ElectronFile, + fileTypeInfo: FileTypeInfo +) { + let thumbnail: Uint8Array; + try { + addLogLine( + `ffmpeg generateThumbnail called for ${getFileNameSize(file)}` + ); + + const thumbnail = await FFmpegService.generateVideoThumbnail(file); + addLogLine( + `ffmpeg thumbnail successfully generated ${getFileNameSize(file)}` + ); + return getUint8ArrayView(thumbnail); + } catch (e) { + addLogLine( + `ffmpeg thumbnail generated failed ${getFileNameSize( + file + )} error: ${e.message}` + ); + logError(e, 'failed to generate thumbnail using ffmpeg', { + fileFormat: fileTypeInfo.exactType, + }); + thumbnail = await generateVideoThumbnailUsingCanvas(file); + } + return thumbnail; +} + +export async function generateVideoThumbnailUsingCanvas( + file: File | ElectronFile +) { const canvas = document.createElement('canvas'); const canvasCTX = canvas.getContext('2d'); - let videoURL = null; let timeout = null; + let videoURL = null; + let video = document.createElement('video'); + videoURL = URL.createObjectURL(new Blob([await file.arrayBuffer()])); await new Promise((resolve, reject) => { - let video = document.createElement('video'); - videoURL = URL.createObjectURL(file); + video.preload = 'metadata'; + video.src = videoURL; video.addEventListener('loadeddata', function () { try { + URL.revokeObjectURL(videoURL); if (!video) { throw Error('video load failed'); } @@ -196,17 +241,16 @@ export async function generateVideoThumbnail(file: File) { reject(err); } }); - video.preload = 'metadata'; - video.src = videoURL; timeout = setTimeout( () => reject(Error(CustomError.WAIT_TIME_EXCEEDED)), WAIT_TIME_THUMBNAIL_GENERATION ); }); - return canvas; + const thumbnailBlob = await getCompressedThumbnailBlobFromCanvas(canvas); + return await getUint8ArrayView(thumbnailBlob); } -async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) { +async function getCompressedThumbnailBlobFromCanvas(canvas: HTMLCanvasElement) { let thumbnailBlob: Blob = null; let prevSize = Number.MAX_SAFE_INTEGER; let quality = MAX_QUALITY; @@ -232,13 +276,6 @@ async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) { percentageSizeDiff(thumbnailBlob.size, prevSize) >= MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF ); - if (thumbnailBlob.size > MAX_THUMBNAIL_SIZE) { - logError( - Error('thumbnail_too_large'), - 'thumbnail greater than max limit', - { thumbnailSize: convertBytesToHumanReadable(thumbnailBlob.size) } - ); - } return thumbnailBlob; } diff --git a/src/services/upload/uiService.ts b/src/services/upload/uiService.ts index 70c0b036e..31be1f7c8 100644 --- a/src/services/upload/uiService.ts +++ b/src/services/upload/uiService.ts @@ -70,6 +70,10 @@ class UIService { this.updateProgressBarUI(); } + removeFromInProgressList(key: number) { + this.inProgressUploads.delete(key); + } + moveFileToResultList(key: number, uploadResult: UPLOAD_RESULT) { this.finishedUploads.set(key, uploadResult); this.inProgressUploads.delete(key); diff --git a/src/services/upload/uploadHttpClient.ts b/src/services/upload/uploadHttpClient.ts index ecf3d81e8..351031ae8 100644 --- a/src/services/upload/uploadHttpClient.ts +++ b/src/services/upload/uploadHttpClient.ts @@ -51,7 +51,9 @@ class UploadHttpClient { { 'X-Auth-Token': token } ); const response = await this.uploadURLFetchInProgress; - urlStore.push(...response.data['urls']); + for (const url of response.data['urls']) { + urlStore.push(url); + } } finally { this.uploadURLFetchInProgress = null; } diff --git a/src/services/upload/uploadManager.ts b/src/services/upload/uploadManager.ts index 846003997..838fdd6e8 100644 --- a/src/services/upload/uploadManager.ts +++ b/src/services/upload/uploadManager.ts @@ -1,6 +1,5 @@ import { getLocalFiles } from '../fileService'; import { SetFiles } from 'types/gallery'; -import { getDedicatedCryptoWorker } from 'utils/crypto'; import { sortFiles, preservePhotoswipeProps, @@ -19,23 +18,15 @@ import UploadService from './uploadService'; import { eventBus, Events } from 'services/events'; import { CustomError } from 'utils/error'; import { Collection } from 'types/collection'; -import { EnteFile } from 'types/file'; +import { EncryptedEnteFile, EnteFile } from 'types/file'; import { - ElectronFile, FileWithCollection, - Metadata, - MetadataAndFileTypeInfo, - MetadataAndFileTypeInfoMap, ParsedMetadataJSON, ParsedMetadataJSONMap, + PublicUploadProps, } from 'types/upload'; -import { - UPLOAD_RESULT, - MAX_FILE_SIZE_SUPPORTED, - UPLOAD_STAGES, -} from 'constants/upload'; -import { ComlinkWorker } from 'utils/comlink'; -import { FILE_TYPE } from 'constants/file'; + +import { UPLOAD_RESULT, UPLOAD_STAGES } from 'constants/upload'; import uiService from './uiService'; import { addLogLine, getFileNameSize } from 'utils/logging'; import isElectron from 'is-electron'; @@ -43,14 +34,23 @@ import ImportService from 'services/importService'; import watchFolderService from 'services/watchFolder/watchFolderService'; import { ProgressUpdater } from 'types/upload/ui'; import uploadCancelService from './uploadCancelService'; +import { DedicatedCryptoWorker } from 'worker/crypto.worker'; +import { ComlinkWorker } from 'utils/comlink/comlinkWorker'; +import { Remote } from 'comlink'; +import { + getLocalPublicFiles, + getPublicCollectionUID, +} from 'services/publicCollectionService'; +import { getDedicatedCryptoWorker } from 'utils/comlink/ComlinkCryptoWorker'; const MAX_CONCURRENT_UPLOADS = 4; const FILE_UPLOAD_COMPLETED = 100; class UploadManager { - private cryptoWorkers = new Array(MAX_CONCURRENT_UPLOADS); + private cryptoWorkers = new Array< + ComlinkWorker + >(MAX_CONCURRENT_UPLOADS); private parsedMetadataJSONMap: ParsedMetadataJSONMap; - private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap; private filesToBeUploaded: FileWithCollection[]; private remainingFiles: FileWithCollection[] = []; private failedFiles: FileWithCollection[]; @@ -59,12 +59,18 @@ class UploadManager { private setFiles: SetFiles; private collections: Map; private uploadInProgress: boolean; + private publicUploadProps: PublicUploadProps; + private uploaderName: string; - public async init(progressUpdater: ProgressUpdater, setFiles: SetFiles) { - UIService.init(progressUpdater); - this.setFiles = setFiles; + public async init( + progressUpdater: ProgressUpdater, + setFiles: SetFiles, + publicCollectProps: PublicUploadProps + ) { UIService.init(progressUpdater); + UploadService.init(publicCollectProps); this.setFiles = setFiles; + this.publicUploadProps = publicCollectProps; } public isUploadRunning() { @@ -76,10 +82,8 @@ class UploadManager { this.remainingFiles = []; this.failedFiles = []; this.parsedMetadataJSONMap = new Map(); - this.metadataAndFileTypeInfoMap = new Map< - number, - MetadataAndFileTypeInfo - >(); + + this.uploaderName = null; } prepareForNewUpload() { @@ -90,10 +94,17 @@ class UploadManager { } async updateExistingFilesAndCollections(collections: Collection[]) { - this.existingFiles = await getLocalFiles(); - this.userOwnedNonTrashedExistingFiles = getUserOwnedNonTrashedFiles( - this.existingFiles - ); + if (this.publicUploadProps.accessedThroughSharedURL) { + this.existingFiles = await getLocalPublicFiles( + getPublicCollectionUID(this.publicUploadProps.token) + ); + this.userOwnedNonTrashedExistingFiles = this.existingFiles; + } else { + this.existingFiles = await getLocalFiles(); + this.userOwnedNonTrashedExistingFiles = getUserOwnedNonTrashedFiles( + this.existingFiles + ); + } this.collections = new Map( collections.map((collection) => [collection.id, collection]) ); @@ -101,7 +112,8 @@ class UploadManager { public async queueFilesForUpload( filesWithCollectionToUploadIn: FileWithCollection[], - collections: Collection[] + collections: Collection[], + uploaderName?: string ) { try { if (this.uploadInProgress) { @@ -109,9 +121,18 @@ class UploadManager { } this.uploadInProgress = true; await this.updateExistingFilesAndCollections(collections); + this.uploaderName = uploaderName; addLogLine( `received ${filesWithCollectionToUploadIn.length} files to upload` ); + uiService.setFilenames( + new Map( + filesWithCollectionToUploadIn.map((mediaFile) => [ + mediaFile.localID, + UploadService.getAssetName(mediaFile), + ]) + ) + ); const { metadataJSONFiles, mediaFiles } = segregateMetadataAndMediaFiles(filesWithCollectionToUploadIn); addLogLine(`has ${metadataJSONFiles.length} metadata json files`); @@ -127,47 +148,18 @@ class UploadManager { ); } if (mediaFiles.length) { - UIService.setUploadStage(UPLOAD_STAGES.EXTRACTING_METADATA); - await this.extractMetadataFromFiles(mediaFiles); - - UploadService.setMetadataAndFileTypeInfoMap( - this.metadataAndFileTypeInfoMap - ); - - addLogLine(`clusterLivePhotoFiles called`); - - // filter out files whose metadata detection failed or those that have been skipped because the files are too large, - // as they will be rejected during upload and are not valid upload files which we need to clustering - const rejectedFileLocalIDs = new Set( - [...this.metadataAndFileTypeInfoMap.entries()].map( - ([localID, metadataAndFileTypeInfo]) => { - if ( - !metadataAndFileTypeInfo.metadata || - !metadataAndFileTypeInfo.fileTypeInfo - ) { - return localID; - } - } - ) - ); - const rejectedFiles = []; - const filesWithMetadata = []; - mediaFiles.forEach((m) => { - if (rejectedFileLocalIDs.has(m.localID)) { - rejectedFiles.push(m); - } else { - filesWithMetadata.push(m); - } - }); - + addLogLine(`clusterLivePhotoFiles started`); const analysedMediaFiles = - UploadService.clusterLivePhotoFiles(filesWithMetadata); - - const allFiles = [...rejectedFiles, ...analysedMediaFiles]; - + await UploadService.clusterLivePhotoFiles(mediaFiles); + addLogLine(`clusterLivePhotoFiles ended`); + addLogLine( + `got live photos: ${ + mediaFiles.length !== analysedMediaFiles.length + }` + ); uiService.setFilenames( new Map( - allFiles.map((mediaFile) => [ + analysedMediaFiles.map((mediaFile) => [ mediaFile.localID, UploadService.getAssetName(mediaFile), ]) @@ -175,13 +167,10 @@ class UploadManager { ); UIService.setHasLivePhoto( - mediaFiles.length !== allFiles.length - ); - addLogLine( - `got live photos: ${mediaFiles.length !== allFiles.length}` + mediaFiles.length !== analysedMediaFiles.length ); - await this.uploadMediaFiles(allFiles); + await this.uploadMediaFiles(analysedMediaFiles); } } catch (e) { if (e.message === CustomError.UPLOAD_CANCELLED) { @@ -190,17 +179,13 @@ class UploadManager { } } else { logError(e, 'uploading failed with error'); - addLogLine( - `uploading failed with error -> ${e.message} - ${(e as Error).stack}` - ); throw e; } } finally { UIService.setUploadStage(UPLOAD_STAGES.FINISH); UIService.setPercentComplete(FILE_UPLOAD_COMPLETED); for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) { - this.cryptoWorkers[i]?.worker.terminate(); + this.cryptoWorkers[i]?.terminate(); } this.uploadInProgress = false; } @@ -254,12 +239,12 @@ class UploadManager { } else { // and don't break for subsequent files just log and move on logError(e, 'parsing failed for a file'); + addLogLine( + `failed to parse metadata json file ${getFileNameSize( + file + )} error: ${e.message}` + ); } - addLogLine( - `failed to parse metadata json file ${getFileNameSize( - file - )} error: ${e.message}` - ); } } } catch (e) { @@ -270,103 +255,12 @@ class UploadManager { } } - private async extractMetadataFromFiles(mediaFiles: FileWithCollection[]) { - try { - addLogLine(`extractMetadataFromFiles executed`); - UIService.reset(mediaFiles.length); - for (const { file, localID, collectionID } of mediaFiles) { - if (uploadCancelService.isUploadCancelationRequested()) { - throw Error(CustomError.UPLOAD_CANCELLED); - } - let fileTypeInfo = null; - let metadata = null; - let filePath = null; - try { - addLogLine( - `metadata extraction started ${getFileNameSize(file)} ` - ); - const result = await this.extractFileTypeAndMetadata( - file, - collectionID - ); - fileTypeInfo = result.fileTypeInfo; - metadata = result.metadata; - filePath = result.filePath; - addLogLine( - `metadata extraction successful${getFileNameSize( - file - )} ` - ); - } catch (e) { - if (e.message === CustomError.UPLOAD_CANCELLED) { - throw e; - } else { - // and don't break for subsequent files just log and move on - logError(e, 'extractFileTypeAndMetadata failed'); - } - addLogLine( - `metadata extraction failed ${getFileNameSize( - file - )} error: ${e.message}` - ); - } - this.metadataAndFileTypeInfoMap.set(localID, { - fileTypeInfo: fileTypeInfo && { ...fileTypeInfo }, - metadata: metadata && { ...metadata }, - filePath: filePath, - }); - UIService.increaseFileUploaded(); - } - } catch (e) { - if (e.message !== CustomError.UPLOAD_CANCELLED) { - logError(e, 'error extracting metadata'); - } - throw e; - } - } - - private async extractFileTypeAndMetadata( - file: File | ElectronFile, - collectionID: number - ) { - if (file.size >= MAX_FILE_SIZE_SUPPORTED) { - addLogLine( - `${getFileNameSize(file)} rejected because of large size` - ); - - return { fileTypeInfo: null, metadata: null }; - } - const fileTypeInfo = await UploadService.getFileType(file); - if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) { - addLogLine( - `${getFileNameSize( - file - )} rejected because of unknown file format` - ); - return { fileTypeInfo, metadata: null }; - } - addLogLine(` extracting ${getFileNameSize(file)} metadata`); - let metadata: Metadata; - try { - metadata = await UploadService.extractFileMetadata( - file, - collectionID, - fileTypeInfo - ); - const filePath = (file as any).path as string; - return { fileTypeInfo, metadata, filePath }; - } catch (e) { - logError(e, 'failed to extract file metadata'); - return { fileTypeInfo, metadata: null, filePath: null }; - } - } - private async uploadMediaFiles(mediaFiles: FileWithCollection[]) { addLogLine(`uploadMediaFiles called`); - this.filesToBeUploaded.push(...mediaFiles); + this.filesToBeUploaded = [...this.filesToBeUploaded, ...mediaFiles]; if (isElectron()) { - this.remainingFiles.push(...mediaFiles); + this.remainingFiles = [...this.remainingFiles, ...mediaFiles]; } UIService.reset(mediaFiles.length); @@ -381,21 +275,14 @@ class UploadManager { i < MAX_CONCURRENT_UPLOADS && this.filesToBeUploaded.length > 0; i++ ) { - const cryptoWorker = getDedicatedCryptoWorker(); - if (!cryptoWorker) { - throw Error(CustomError.FAILED_TO_LOAD_WEB_WORKER); - } - this.cryptoWorkers[i] = cryptoWorker; - uploadProcesses.push( - this.uploadNextFileInQueue( - await new this.cryptoWorkers[i].comlink() - ) - ); + this.cryptoWorkers[i] = getDedicatedCryptoWorker(); + const worker = await new this.cryptoWorkers[i].remote(); + uploadProcesses.push(this.uploadNextFileInQueue(worker)); } await Promise.all(uploadProcesses); } - private async uploadNextFileInQueue(worker: any) { + private async uploadNextFileInQueue(worker: Remote) { while (this.filesToBeUploaded.length > 0) { if (uploadCancelService.isUploadCancelationRequested()) { throw Error(CustomError.UPLOAD_CANCELLED); @@ -407,7 +294,9 @@ class UploadManager { const { fileUploadResult, uploadedFile } = await uploader( worker, this.userOwnedNonTrashedExistingFiles, - fileWithCollection + fileWithCollection, + this.uploaderName, + this.publicUploadProps?.accessedThroughSharedURL ); const finalUploadResult = await this.postUploadTask( @@ -426,12 +315,14 @@ class UploadManager { async postUploadTask( fileUploadResult: UPLOAD_RESULT, - uploadedFile: EnteFile | null, + uploadedFile: EncryptedEnteFile | EnteFile | null, fileWithCollection: FileWithCollection ) { try { let decryptedFile: EnteFile; - addLogLine(`uploadedFile ${JSON.stringify(uploadedFile)}`); + addLogLine( + `post upload action -> fileUploadResult: ${fileUploadResult} uploadedFile present ${!!uploadedFile}` + ); this.updateElectronRemainingFiles(fileWithCollection); switch (fileUploadResult) { case UPLOAD_RESULT.FAILED: @@ -439,22 +330,23 @@ class UploadManager { this.failedFiles.push(fileWithCollection); break; case UPLOAD_RESULT.ALREADY_UPLOADED: - decryptedFile = uploadedFile; + decryptedFile = uploadedFile as EnteFile; break; case UPLOAD_RESULT.ADDED_SYMLINK: - decryptedFile = uploadedFile; + decryptedFile = uploadedFile as EnteFile; fileUploadResult = UPLOAD_RESULT.UPLOADED; break; case UPLOAD_RESULT.UPLOADED: case UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL: decryptedFile = await decryptFile( - uploadedFile, + uploadedFile as EncryptedEnteFile, fileWithCollection.collection.key ); break; case UPLOAD_RESULT.UNSUPPORTED: case UPLOAD_RESULT.TOO_LARGE: case UPLOAD_RESULT.CANCELLED: + case UPLOAD_RESULT.SKIPPED_VIDEOS: // no-op break; default: @@ -480,15 +372,11 @@ class UploadManager { await this.watchFolderCallback( fileUploadResult, fileWithCollection, - uploadedFile + uploadedFile as EncryptedEnteFile ); return fileUploadResult; } catch (e) { logError(e, 'failed to do post file upload action'); - addLogLine( - `failed to do post file upload action -> ${e.message} - ${(e as Error).stack}` - ); return UPLOAD_RESULT.FAILED; } } @@ -496,7 +384,7 @@ class UploadManager { private async watchFolderCallback( fileUploadResult: UPLOAD_RESULT, fileWithCollection: FileWithCollection, - uploadedFile: EnteFile + uploadedFile: EncryptedEnteFile ) { if (isElectron()) { await watchFolderService.onFileUpload( @@ -508,17 +396,22 @@ class UploadManager { } public cancelRunningUpload() { + addLogLine('user cancelled running upload'); UIService.setUploadStage(UPLOAD_STAGES.CANCELLING); uploadCancelService.requestUploadCancelation(); } - async getFailedFilesWithCollections() { + getFailedFilesWithCollections() { return { files: this.failedFiles, collections: [...this.collections.values()], }; } + getUploaderName() { + return this.uploaderName; + } + private updateExistingFiles(decryptedFile: EnteFile) { if (!decryptedFile) { throw Error("decrypted file can't be undefined"); diff --git a/src/services/upload/uploadService.ts b/src/services/upload/uploadService.ts index 429dfde98..76d05e6e5 100644 --- a/src/services/upload/uploadService.ts +++ b/src/services/upload/uploadService.ts @@ -5,26 +5,25 @@ import { extractFileMetadata, getFilename } from './fileService'; import { getFileType } from '../typeDetectionService'; import { CustomError, handleUploadError } from 'utils/error'; import { - B64EncryptionResult, BackupedFile, - ElectronFile, EncryptedFile, FileTypeInfo, FileWithCollection, FileWithMetadata, isDataStream, Metadata, - MetadataAndFileTypeInfo, - MetadataAndFileTypeInfoMap, ParsedMetadataJSON, ParsedMetadataJSONMap, ProcessedFile, + PublicUploadProps, UploadAsset, UploadFile, UploadURL, } from 'types/upload'; import { clusterLivePhotoFiles, + extractLivePhotoMetadata, + getLivePhotoFileType, getLivePhotoName, getLivePhotoSize, readLivePhoto, @@ -33,6 +32,12 @@ import { encryptFile, getFileSize, readFile } from './fileService'; import { uploadStreamUsingMultipart } from './multiPartUploadService'; import UIService from './uiService'; import { USE_CF_PROXY } from 'constants/upload'; +import { Remote } from 'comlink'; +import { DedicatedCryptoWorker } from 'worker/crypto.worker'; +import publicUploadHttpClient from './publicUploadHttpClient'; +import { constructPublicMagicMetadata } from './magicMetadataService'; +import { FilePublicMagicMetadataProps } from 'types/magicMetadata'; +import { B64EncryptionResult } from 'types/crypto'; class UploadService { private uploadURLs: UploadURL[] = []; @@ -40,26 +45,32 @@ class UploadService { string, ParsedMetadataJSON >(); - private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap = new Map< - number, - MetadataAndFileTypeInfo - >(); + + private uploaderName: string; private pendingUploadCount: number = 0; + private publicUploadProps: PublicUploadProps = undefined; + + init(publicUploadProps: PublicUploadProps) { + this.publicUploadProps = publicUploadProps; + } + async setFileCount(fileCount: number) { this.pendingUploadCount = fileCount; - await this.preFetchUploadURLs(); + this.preFetchUploadURLs(); } setParsedMetadataJSONMap(parsedMetadataJSONMap: ParsedMetadataJSONMap) { this.parsedMetadataJSONMap = parsedMetadataJSONMap; } - setMetadataAndFileTypeInfoMap( - metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap - ) { - this.metadataAndFileTypeInfoMap = metadataAndFileTypeInfoMap; + setUploaderName(uploaderName: string) { + this.uploaderName = uploaderName; + } + + getUploaderName() { + return this.uploaderName; } reducePendingUploadCount() { @@ -72,14 +83,16 @@ class UploadService { : getFileSize(file); } - getAssetName({ isLivePhoto, file, livePhotoAssets }: FileWithCollection) { + getAssetName({ isLivePhoto, file, livePhotoAssets }: UploadAsset) { return isLivePhoto - ? getLivePhotoName(livePhotoAssets.image.name) + ? getLivePhotoName(livePhotoAssets) : getFilename(file); } - async getFileType(file: File | ElectronFile) { - return getFileType(file); + getAssetFileType({ isLivePhoto, file, livePhotoAssets }: UploadAsset) { + return isLivePhoto + ? getLivePhotoFileType(livePhotoAssets) + : getFileType(file); } async readAsset( @@ -91,39 +104,41 @@ class UploadService { : await readFile(fileTypeInfo, file); } - async extractFileMetadata( - file: File | ElectronFile, + async extractAssetMetadata( + worker, + { isLivePhoto, file, livePhotoAssets }: UploadAsset, collectionID: number, fileTypeInfo: FileTypeInfo ): Promise { - return extractFileMetadata( - this.parsedMetadataJSONMap, - file, - collectionID, - fileTypeInfo - ); - } - - getFileMetadataAndFileTypeInfo(localID: number) { - return this.metadataAndFileTypeInfoMap.get(localID); - } - - setFileMetadataAndFileTypeInfo( - localID: number, - metadataAndFileTypeInfo: MetadataAndFileTypeInfo - ) { - return this.metadataAndFileTypeInfoMap.set( - localID, - metadataAndFileTypeInfo - ); + return isLivePhoto + ? extractLivePhotoMetadata( + worker, + this.parsedMetadataJSONMap, + collectionID, + fileTypeInfo, + livePhotoAssets + ) + : await extractFileMetadata( + worker, + this.parsedMetadataJSONMap, + collectionID, + fileTypeInfo, + file + ); } clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) { return clusterLivePhotoFiles(mediaFiles); } + constructPublicMagicMetadata( + publicMagicMetadataProps: FilePublicMagicMetadataProps + ) { + return constructPublicMagicMetadata(publicMagicMetadataProps); + } + async encryptAsset( - worker: any, + worker: Remote, file: FileWithMetadata, encryptionKey: string ): Promise { @@ -146,13 +161,13 @@ class UploadService { if (USE_CF_PROXY) { fileObjectKey = await UploadHttpClient.putFileV2( fileUploadURL, - file.file.encryptedData, + file.file.encryptedData as Uint8Array, progressTracker ); } else { fileObjectKey = await UploadHttpClient.putFile( fileUploadURL, - file.file.encryptedData, + file.file.encryptedData as Uint8Array, progressTracker ); } @@ -162,13 +177,13 @@ class UploadService { if (USE_CF_PROXY) { thumbnailObjectKey = await UploadHttpClient.putFileV2( thumbnailUploadURL, - file.thumbnail.encryptedData as Uint8Array, + file.thumbnail.encryptedData, null ); } else { thumbnailObjectKey = await UploadHttpClient.putFile( thumbnailUploadURL, - file.thumbnail.encryptedData as Uint8Array, + file.thumbnail.encryptedData, null ); } @@ -183,6 +198,7 @@ class UploadService { objectKey: thumbnailObjectKey, }, metadata: file.metadata, + pubMagicMetadata: file.pubMagicMetadata, }; return backupedFile; } catch (e) { @@ -225,11 +241,32 @@ class UploadService { } } + async uploadFile(uploadFile: UploadFile) { + if (this.publicUploadProps.accessedThroughSharedURL) { + return publicUploadHttpClient.uploadFile( + uploadFile, + this.publicUploadProps.token, + this.publicUploadProps.passwordToken + ); + } else { + return UploadHttpClient.uploadFile(uploadFile); + } + } + private async fetchUploadURLs() { - await UploadHttpClient.fetchUploadURLs( - this.pendingUploadCount, - this.uploadURLs - ); + if (this.publicUploadProps.accessedThroughSharedURL) { + await publicUploadHttpClient.fetchUploadURLs( + this.pendingUploadCount, + this.uploadURLs, + this.publicUploadProps.token, + this.publicUploadProps.passwordToken + ); + } else { + await UploadHttpClient.fetchUploadURLs( + this.pendingUploadCount, + this.uploadURLs + ); + } } } diff --git a/src/services/upload/uploader.ts b/src/services/upload/uploader.ts index 40cdad7ac..f2d2cdab0 100644 --- a/src/services/upload/uploader.ts +++ b/src/services/upload/uploader.ts @@ -2,17 +2,26 @@ import { EnteFile } from 'types/file'; import { handleUploadError, CustomError } from 'utils/error'; import { logError } from 'utils/sentry'; import { findMatchingExistingFiles } from 'utils/upload'; -import UploadHttpClient from './uploadHttpClient'; import UIService from './uiService'; import UploadService from './uploadService'; import { FILE_TYPE } from 'constants/file'; import { UPLOAD_RESULT, MAX_FILE_SIZE_SUPPORTED } from 'constants/upload'; -import { FileWithCollection, BackupedFile, UploadFile } from 'types/upload'; +import { + FileWithCollection, + BackupedFile, + UploadFile, + FileWithMetadata, + FileTypeInfo, +} from 'types/upload'; import { addLocalLog, addLogLine } from 'utils/logging'; import { convertBytesToHumanReadable } from 'utils/file/size'; import { sleep } from 'utils/common'; import { addToCollection } from 'services/collectionService'; import uploadCancelService from './uploadCancelService'; +import { Remote } from 'comlink'; +import { DedicatedCryptoWorker } from 'worker/crypto.worker'; +import uploadService from './uploadService'; +import { FilePublicMagicMetadata } from 'types/magicMetadata'; interface UploadResponse { fileUploadResult: UPLOAD_RESULT; @@ -20,9 +29,11 @@ interface UploadResponse { } export default async function uploader( - worker: any, + worker: Remote, existingFiles: EnteFile[], - fileWithCollection: FileWithCollection + fileWithCollection: FileWithCollection, + uploaderName: string, + skipVideos: boolean ): Promise { const { collection, localID, ...uploadAsset } = fileWithCollection; const fileNameSize = `${UploadService.getAssetName( @@ -32,20 +43,33 @@ export default async function uploader( addLogLine(`uploader called for ${fileNameSize}`); UIService.setFileProgress(localID, 0); await sleep(0); - const { fileTypeInfo, metadata } = - UploadService.getFileMetadataAndFileTypeInfo(localID); + let fileTypeInfo: FileTypeInfo; try { const fileSize = UploadService.getAssetSize(uploadAsset); if (fileSize >= MAX_FILE_SIZE_SUPPORTED) { return { fileUploadResult: UPLOAD_RESULT.TOO_LARGE }; } + addLogLine(`getting filetype for ${fileNameSize}`); + fileTypeInfo = await UploadService.getAssetFileType(uploadAsset); + addLogLine(`got filetype for ${fileNameSize}`); if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) { throw Error(CustomError.UNSUPPORTED_FILE_FORMAT); } - if (!metadata) { - throw Error(CustomError.NO_METADATA); + if (skipVideos && fileTypeInfo.fileType === FILE_TYPE.VIDEO) { + addLogLine( + `skipped video upload for public upload ${fileNameSize}` + ); + return { fileUploadResult: UPLOAD_RESULT.SKIPPED_VIDEOS }; } + addLogLine(`extracting metadata ${fileNameSize}`); + const metadata = await UploadService.extractAssetMetadata( + worker, + uploadAsset, + collection.id, + fileTypeInfo + ); + const matchingExistingFiles = findMatchingExistingFiles( existingFiles, metadata @@ -100,11 +124,18 @@ export default async function uploader( if (file.hasStaticThumbnail) { metadata.hasStaticThumbnail = true; } - const fileWithMetadata = { + let pubMagicMetadata: FilePublicMagicMetadata; + if (uploaderName) { + pubMagicMetadata = await uploadService.constructPublicMagicMetadata( + { uploaderName } + ); + } + const fileWithMetadata: FileWithMetadata = { localID, filedata: file.filedata, thumbnail: file.thumbnail, metadata, + pubMagicMetadata, }; if (uploadCancelService.isUploadCancelationRequested()) { @@ -131,9 +162,8 @@ export default async function uploader( backupedFile, encryptedFile.fileKey ); - addLogLine(`uploadFile ${fileNameSize}`); - const uploadedFile = await UploadHttpClient.uploadFile(uploadFile); + const uploadedFile = await UploadService.uploadFile(uploadFile); UIService.increaseFileUploaded(); addLogLine(`${fileNameSize} successfully uploaded`); diff --git a/src/services/upload/videoMetadataService.ts b/src/services/upload/videoMetadataService.ts index 76505d356..a7c2e2e34 100644 --- a/src/services/upload/videoMetadataService.ts +++ b/src/services/upload/videoMetadataService.ts @@ -1,5 +1,5 @@ import { NULL_EXTRACTED_METADATA } from 'constants/upload'; -import ffmpegService from 'services/ffmpeg/ffmpegService'; +import * as ffmpegService from 'services/ffmpeg/ffmpegService'; import { ElectronFile } from 'types/upload'; import { logError } from 'utils/sentry'; import { getFileNameSize, addLogLine } from 'utils/logging'; @@ -8,16 +8,7 @@ export async function getVideoMetadata(file: File | ElectronFile) { let videoMetadata = NULL_EXTRACTED_METADATA; try { addLogLine(`getVideoMetadata called for ${getFileNameSize(file)}`); - if (!(file instanceof File)) { - addLogLine('get file blob for video metadata extraction'); - file = new File([await file.blob()], file.name, { - lastModified: file.lastModified, - }); - addLogLine( - 'get file blob for video metadata extraction successfully' - ); - } - videoMetadata = await ffmpegService.extractMetadata(file); + videoMetadata = await ffmpegService.extractVideoMetadata(file); addLogLine( `videoMetadata successfully extracted ${getFileNameSize(file)}` ); diff --git a/src/services/userService.ts b/src/services/userService.ts index afb50bd8a..f1725d3aa 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -6,7 +6,7 @@ import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage'; import localForage from 'utils/storage/localForage'; import { getToken } from 'utils/common/key'; import HTTPService from './HTTPService'; -import { B64EncryptionResult } from 'utils/crypto'; +import { getRecoveryKey } from 'utils/crypto'; import { logError } from 'utils/sentry'; import { eventBus, Events } from './events'; import { @@ -24,6 +24,7 @@ import { ServerErrorCodes } from 'utils/error'; import isElectron from 'is-electron'; import safeStorageService from './electron/safeStorage'; import { deleteAllCache } from 'utils/storage/cache'; +import { B64EncryptionResult } from 'types/crypto'; const ENDPOINT = getEndpoint(); @@ -367,3 +368,13 @@ export const deleteAccount = async (challenge: string) => { throw e; } }; + +export const validateKey = async () => { + try { + await getRecoveryKey(); + return true; + } catch (e) { + await logoutUser(); + return false; + } +}; diff --git a/src/services/wasm/ffmpeg.ts b/src/services/wasm/ffmpeg.ts new file mode 100644 index 000000000..63d450861 --- /dev/null +++ b/src/services/wasm/ffmpeg.ts @@ -0,0 +1,107 @@ +import { createFFmpeg, FFmpeg } from 'ffmpeg-wasm'; +import QueueProcessor from 'services/queueProcessor'; +import { getUint8ArrayView } from 'services/readerService'; +import { promiseWithTimeout } from 'utils/common'; +import { addLogLine } from 'utils/logging'; +import { logError } from 'utils/sentry'; +import { generateTempName } from 'utils/temp'; + +const INPUT_PATH_PLACEHOLDER = 'INPUT'; +const FFMPEG_PLACEHOLDER = 'FFMPEG'; +const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT'; + +const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000; + +export class WasmFFmpeg { + private ffmpeg: FFmpeg; + private ready: Promise = null; + private ffmpegTaskQueue = new QueueProcessor(1); + + constructor() { + this.ffmpeg = createFFmpeg({ + corePath: '/js/ffmpeg/ffmpeg-core.js', + mt: false, + }); + + this.ready = this.init(); + } + + private async init() { + if (!this.ffmpeg.isLoaded()) { + await this.ffmpeg.load(); + } + } + + async run(cmd: string[], inputFile: File, outputFileName: string) { + const response = this.ffmpegTaskQueue.queueUpRequest(() => + promiseWithTimeout( + this.execute(cmd, inputFile, outputFileName), + FFMPEG_EXECUTION_WAIT_TIME + ) + ); + try { + return await response.promise; + } catch (e) { + logError(e, 'ffmpeg run failed'); + throw e; + } + } + + private async execute( + cmd: string[], + inputFile: File, + outputFileName: string + ) { + let tempInputFilePath: string; + let tempOutputFilePath: string; + try { + await this.ready; + const extension = getFileExtension(inputFile.name); + const tempNameSuffix = extension ? `input.${extension}` : 'input'; + tempInputFilePath = `${generateTempName(10, tempNameSuffix)}`; + this.ffmpeg.FS( + 'writeFile', + tempInputFilePath, + await getUint8ArrayView(inputFile) + ); + tempOutputFilePath = `${generateTempName(10, outputFileName)}`; + + cmd = cmd.map((cmdPart) => { + if (cmdPart === FFMPEG_PLACEHOLDER) { + return ''; + } else if (cmdPart === INPUT_PATH_PLACEHOLDER) { + return tempInputFilePath; + } else if (cmdPart === OUTPUT_PATH_PLACEHOLDER) { + return tempOutputFilePath; + } else { + return cmdPart; + } + }); + addLogLine(`${cmd}`); + await this.ffmpeg.run(...cmd); + return new File( + [this.ffmpeg.FS('readFile', tempOutputFilePath)], + outputFileName + ); + } finally { + try { + this.ffmpeg.FS('unlink', tempInputFilePath); + } catch (e) { + logError(e, 'unlink input file failed'); + } + try { + this.ffmpeg.FS('unlink', tempOutputFilePath); + } catch (e) { + logError(e, 'unlink output file failed'); + } + } + } +} + +function getFileExtension(filename: string) { + const lastDotPosition = filename.lastIndexOf('.'); + if (lastDotPosition === -1) return null; + else { + return filename.slice(lastDotPosition + 1); + } +} diff --git a/src/services/heicConverter/heicConverterClient.ts b/src/services/wasmHeicConverter/wasmHEICConverterClient.ts similarity index 100% rename from src/services/heicConverter/heicConverterClient.ts rename to src/services/wasmHeicConverter/wasmHEICConverterClient.ts diff --git a/src/services/heicConverter/heicConverterService.ts b/src/services/wasmHeicConverter/wasmHEICConverterService.ts similarity index 78% rename from src/services/heicConverter/heicConverterService.ts rename to src/services/wasmHeicConverter/wasmHEICConverterService.ts index 45b4aae90..86212c578 100644 --- a/src/services/heicConverter/heicConverterService.ts +++ b/src/services/wasmHeicConverter/wasmHEICConverterService.ts @@ -1,38 +1,42 @@ import QueueProcessor from 'services/queueProcessor'; import { CustomError } from 'utils/error'; -import { createNewConvertWorker } from 'utils/heicConverter'; import { retryAsyncFunction } from 'utils/network'; import { logError } from 'utils/sentry'; import { addLogLine } from 'utils/logging'; +import { DedicatedConvertWorker } from 'worker/convert.worker'; +import { ComlinkWorker } from 'utils/comlink/comlinkWorker'; import { convertBytesToHumanReadable } from 'utils/file/size'; +import { getDedicatedConvertWorker } from 'utils/comlink/ComlinkConvertWorker'; const WORKER_POOL_SIZE = 2; const MAX_CONVERSION_IN_PARALLEL = 1; const WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS = [100, 100]; const WAIT_TIME_IN_MICROSECONDS = 30 * 1000; const BREATH_TIME_IN_MICROSECONDS = 1000; +const CONVERT_FORMAT = 'JPEG'; class HEICConverter { private convertProcessor = new QueueProcessor( MAX_CONVERSION_IN_PARALLEL ); - private workerPool: { comlink: any; worker: Worker }[]; + private workerPool: ComlinkWorker[] = []; private ready: Promise; constructor() { this.ready = this.init(); } - async init() { + private async init() { this.workerPool = []; for (let i = 0; i < WORKER_POOL_SIZE; i++) { - this.workerPool.push(await createNewConvertWorker()); + this.workerPool.push(getDedicatedConvertWorker()); } } - async convert(fileBlob: Blob, format = 'JPEG'): Promise { + async convert(fileBlob: Blob): Promise { await this.ready; const response = this.convertProcessor.queueUpRequest(() => retryAsyncFunction(async () => { - const { comlink, worker } = this.workerPool.shift(); + const convertWorker = this.workerPool.shift(); + const worker = await new convertWorker.remote(); try { const convertedHEIC = await new Promise( (resolve, reject) => { @@ -42,10 +46,10 @@ class HEICConverter { reject(Error('wait time exceeded')); }, WAIT_TIME_IN_MICROSECONDS); const startTime = Date.now(); - const convertedHEIC: Blob = - await comlink.convertHEIC( + const convertedHEIC = + await worker.convertHEIC( fileBlob, - format + CONVERT_FORMAT ); addLogLine( `originalFileSize:${convertBytesToHumanReadable( @@ -71,10 +75,10 @@ class HEICConverter { 'converted heic fileSize is Zero', { originalFileSize: convertBytesToHumanReadable( - fileBlob?.size + fileBlob?.size ?? 0 ), convertedFileSize: convertBytesToHumanReadable( - convertedHEIC?.size + convertedHEIC?.size ?? 0 ), } ); @@ -85,13 +89,12 @@ class HEICConverter { BREATH_TIME_IN_MICROSECONDS ); }); - this.workerPool.push({ comlink, worker }); + this.workerPool.push(convertWorker); return convertedHEIC; } catch (e) { - addLogLine('heic conversion failed-' + e.message); logError(e, 'heic conversion failed'); - worker.terminate(); - this.workerPool.push(await createNewConvertWorker()); + convertWorker.terminate(); + this.workerPool.push(getDedicatedConvertWorker()); throw e; } }, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS) diff --git a/src/services/watchFolder/watchFolderEventHandlers.ts b/src/services/watchFolder/watchFolderEventHandlers.ts index 93896ea38..bfc462c83 100644 --- a/src/services/watchFolder/watchFolderEventHandlers.ts +++ b/src/services/watchFolder/watchFolderEventHandlers.ts @@ -22,7 +22,9 @@ export async function diskFileAddedCallback(file: ElectronFile) { files: [file], }; watchFolderService.pushEvent(event); - addLogLine(`added (upload) to event queue, ${JSON.stringify(event)}`); + addLogLine( + `added (upload) to event queue, collectionName:${event.collectionName} folderPath:${event.folderPath}, filesCount: ${event.files.length}` + ); } catch (e) { logError(e, 'error while calling diskFileAddedCallback'); } @@ -46,7 +48,9 @@ export async function diskFileRemovedCallback(filePath: string) { paths: [filePath], }; watchFolderService.pushEvent(event); - addLogLine(`added (trash) to event queue, ${JSON.stringify(event)}`); + addLogLine( + `added (trash) to event queue collectionName:${event.collectionName} folderPath:${event.folderPath} , pathsCount: ${event.paths.length}` + ); } catch (e) { logError(e, 'error while calling diskFileRemovedCallback'); } diff --git a/src/services/watchFolder/watchFolderService.ts b/src/services/watchFolder/watchFolderService.ts index 4ded8cf6d..13dcc7a69 100644 --- a/src/services/watchFolder/watchFolderService.ts +++ b/src/services/watchFolder/watchFolderService.ts @@ -1,5 +1,5 @@ import { Collection } from 'types/collection'; -import { EnteFile } from 'types/file'; +import { EncryptedEnteFile } from 'types/file'; import { ElectronFile, FileWithCollection } from 'types/upload'; import { runningInBrowser } from 'utils/common'; import { removeFromCollection } from '../collectionService'; @@ -33,7 +33,7 @@ class watchFolderService { private trashingDirQueue: string[] = []; private isEventRunning: boolean = false; private uploadRunning: boolean = false; - private filePathToUploadedFileIDMap = new Map(); + private filePathToUploadedFileIDMap = new Map(); private unUploadableFilePaths = new Set(); private isPaused = false; private setElectronFiles: (files: ElectronFile[]) => void; @@ -80,8 +80,6 @@ class watchFolderService { try { let mappings = this.getWatchMappings(); - addLogLine(`mappings, ${mappings.map((m) => JSON.stringify(m))}`); - if (!mappings?.length) { return; } @@ -238,11 +236,6 @@ class watchFolderService { private async runNextEvent() { try { - addLogLine( - `mappings, - ${this.getWatchMappings().map((m) => JSON.stringify(m))}` - ); - if ( this.eventQueue.length === 0 || this.isEventRunning || @@ -252,6 +245,9 @@ class watchFolderService { } const event = this.clubSameCollectionEvents(); + addLogLine( + `running event type:${event.type} collectionName:${event.collectionName} folderPath:${event.folderPath} , fileCount:${event.files?.length} pathsCount: ${event.paths?.length}` + ); const mappings = this.getWatchMappings(); const mapping = mappings.find( (mapping) => mapping.folderPath === event.folderPath @@ -259,15 +255,19 @@ class watchFolderService { if (!mapping) { throw Error('no Mapping found for event'); } + addLogLine( + `mapping for event rootFolder: ${mapping.rootFolderName} folderPath: ${mapping.folderPath} uploadStrategy: ${mapping.uploadStrategy} syncedFilesCount: ${mapping.syncedFiles.length} ignoredFilesCount ${mapping.ignoredFiles.length}` + ); if (event.type === 'upload') { event.files = getValidFilesToUpload(event.files, mapping); + addLogLine(`valid files count: ${event.files?.length}`); if (event.files.length === 0) { return; } } this.currentEvent = event; this.currentlySyncedMapping = mapping; - addLogLine(`running event', ${JSON.stringify(event)}`); + this.setIsEventRunning(true); if (event.type === 'upload') { this.processUploadEvent(); @@ -295,10 +295,10 @@ class watchFolderService { async onFileUpload( fileUploadResult: UPLOAD_RESULT, fileWithCollection: FileWithCollection, - file: EnteFile + file: EncryptedEnteFile ) { addLocalLog(() => `onFileUpload called`); - if (!this.isUploadRunning) { + if (!this.isUploadRunning()) { return; } if ( diff --git a/src/styles/global.css b/src/styles/global.css index f2d2e6271..ba9e35bab 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -121,6 +121,18 @@ html, body { } +.pswp-custom-caption-container { + width: 100%; + display: flex; + justify-content: flex-end; + bottom: 56px; + background-color: transparent !important; +} + +.pswp__caption--empty{ + display: none; +} + .bg-upload-progress-bar { background-color: #51cd7c; } diff --git a/src/styles/search.ts b/src/styles/search.ts index 6e81572a1..81a83f33e 100644 --- a/src/styles/search.ts +++ b/src/styles/search.ts @@ -61,6 +61,6 @@ export const SelectStyles = { color: 'rgba(255, 255, 255, 0.7)', wordSpacing: '2px', whiteSpace: 'nowrap', - marginLeft: '36px', + marginLeft: '40px', }), }; diff --git a/src/themes/darkThemeOptions.tsx b/src/themes/darkThemeOptions.tsx index 2688f5812..d99d36a62 100644 --- a/src/themes/darkThemeOptions.tsx +++ b/src/themes/darkThemeOptions.tsx @@ -9,10 +9,22 @@ declare module '@mui/material/styles' { interface TypeBackground { overPaper?: string; } + + interface BlurStrength { + base: string; + muted: string; + faint: string; + } + interface BlurStrengthOptions { + base?: string; + muted?: string; + faint?: string; + } interface Palette { accent: PaletteColor; fill: PaletteColor; - glass: PaletteColor; + backdrop: PaletteColor; + blur: BlurStrength; danger: PaletteColor; stroke: TypeText; } @@ -20,7 +32,8 @@ declare module '@mui/material/styles' { accent?: PaletteColorOptions; danger?: PaletteColorOptions; fill?: PaletteColorOptions; - glass?: PaletteColorOptions; + backdrop?: PaletteColorOptions; + blur?: BlurStrengthOptions; stroke?: Partial; } @@ -74,6 +87,12 @@ declare module '@mui/material/Alert' { } } +declare module '@mui/material/CircularProgress' { + export interface CircularProgressPropsColorOverrides { + accent: true; + } +} + // Create a theme instance. const darkThemeOptions = createTheme({ components: { @@ -243,6 +262,13 @@ const darkThemeOptions = createTheme({ }, }, }, + MuiSnackbar: { + styleOverrides: { + root: { + borderRadius: '8px', + }, + }, + }, }, palette: { @@ -265,11 +291,15 @@ const darkThemeOptions = createTheme({ dark: 'rgba(256, 256, 256, 0.12)', light: 'rgba(256, 256, 256)', }, - glass: { - main: 'rgba(256, 256, 256, 0.7)', - dark: 'rgba(256, 256, 256, 0.9)', - light: 'rgba(256, 256, 256,0.3)', - contrastText: '#000', + backdrop: { + main: 'rgba(256, 256, 256, 0.65)', + light: 'rgba(0, 0, 0,0.2)', + }, + + blur: { + base: '96px', + muted: '48px', + faint: '24px', }, text: { primary: '#fff', @@ -304,7 +334,7 @@ const darkThemeOptions = createTheme({ typography: { body1: { fontSize: '16px', - lineHeight: '19px', + lineHeight: '20px', }, body2: { fontSize: '14px', @@ -316,7 +346,7 @@ const darkThemeOptions = createTheme({ }, button: { fontSize: '16px', - lineHeight: '19px', + lineHeight: '20px', fontWeight: 'bold', textTransform: 'none', }, diff --git a/src/types/Notification/index.tsx b/src/types/Notification/index.tsx index 22ce43982..553ab20d4 100644 --- a/src/types/Notification/index.tsx +++ b/src/types/Notification/index.tsx @@ -2,11 +2,14 @@ import { ButtonProps } from '@mui/material/Button'; import { ReactNode } from 'react'; export interface NotificationAttributes { - icon?: ReactNode; + startIcon?: ReactNode; variant: ButtonProps['color']; message: JSX.Element | string; - action?: { - text: string; - callback: () => void; - }; + subtext?: JSX.Element | string; + onClick: () => void; + endIcon?: ReactNode; } + +export type SetNotificationAttributes = React.Dispatch< + React.SetStateAction +>; diff --git a/src/types/collection/index.ts b/src/types/collection/index.ts index 73925bbfe..20744b6eb 100644 --- a/src/types/collection/index.ts +++ b/src/types/collection/index.ts @@ -1,25 +1,44 @@ import { User } from 'types/user'; import { EnteFile } from 'types/file'; import { CollectionSummaryType, CollectionType } from 'constants/collection'; -import { MagicMetadataCore, VISIBILITY_STATE } from 'types/magicMetadata'; +import { + EncryptedMagicMetadata, + MagicMetadataCore, + SUB_TYPE, + VISIBILITY_STATE, +} from 'types/magicMetadata'; -export interface Collection { +export interface EncryptedCollection { id: number; owner: User; - key?: string; + // collection name was unencrypted in the past, so we need to keep it as optional name?: string; - encryptedName?: string; - nameDecryptionNonce?: string; + encryptedKey: string; + keyDecryptionNonce: string; + encryptedName: string; + nameDecryptionNonce: string; type: CollectionType; attributes: collectionAttributes; sharees: User[]; - updationTime: number; - encryptedKey: string; - keyDecryptionNonce: string; - isDeleted: boolean; - isSharedCollection?: boolean; publicURLs?: PublicURL[]; - magicMetadata?: CollectionMagicMetadata; + updationTime: number; + isDeleted: boolean; + magicMetadata: EncryptedMagicMetadata; +} + +export interface Collection + extends Omit< + EncryptedCollection, + | 'encryptedKey' + | 'keyDecryptionNonce' + | 'encryptedName' + | 'nameDecryptionNonce' + | 'magicMetadata' + > { + key: string; + name: string; + isSharedCollection?: boolean; + magicMetadata: CollectionMagicMetadata; } export interface PublicURL { @@ -27,6 +46,7 @@ export interface PublicURL { deviceLimit: number; validTill: number; enableDownload: boolean; + enableCollect: boolean; passwordEnabled: boolean; nonce?: string; opsLimit?: number; @@ -37,6 +57,7 @@ export interface UpdatePublicURL { collectionID: number; disablePassword?: boolean; enableDownload?: boolean; + enableCollect?: boolean; validTill?: number; deviceLimit?: number; passHash?: string; @@ -82,6 +103,7 @@ export interface RemoveFromCollectionRequest { export interface CollectionMagicMetadataProps { visibility?: VISIBILITY_STATE; + subType?: SUB_TYPE; } export interface CollectionMagicMetadata diff --git a/src/types/crypto/index.ts b/src/types/crypto/index.ts new file mode 100644 index 000000000..6b5a64b44 --- /dev/null +++ b/src/types/crypto/index.ts @@ -0,0 +1,19 @@ +import { DataStream } from 'types/upload'; + +export interface LocalFileAttributes< + T extends string | Uint8Array | DataStream +> { + encryptedData: T; + decryptionHeader: string; +} + +export interface EncryptionResult { + file: LocalFileAttributes; + key: string; +} + +export interface B64EncryptionResult { + encryptedData: string; + key: string; + nonce: string; +} diff --git a/src/types/dialogBox/index.ts b/src/types/dialogBox/index.ts index 7bf155dda..4350c3388 100644 --- a/src/types/dialogBox/index.ts +++ b/src/types/dialogBox/index.ts @@ -1,6 +1,7 @@ import { ButtonProps } from '@mui/material'; export interface DialogBoxAttributes { + icon?: React.ReactNode; title?: string; staticBackdrop?: boolean; nonClosable?: boolean; diff --git a/src/types/electron/index.ts b/src/types/electron/index.ts index 6c67b5fd1..8eff1ef53 100644 --- a/src/types/electron/index.ts +++ b/src/types/electron/index.ts @@ -2,6 +2,11 @@ import { LimitedCache } from 'types/cache'; import { ElectronFile } from 'types/upload'; import { WatchMapping } from 'types/watchFolder'; +export interface AppUpdateInfo { + autoUpdatable: boolean; + version: string; +} + export interface ElectronAPIs { exists: (path: string) => boolean; checkExistsAndCreateCollectionDir: (dirPath: string) => Promise; @@ -9,7 +14,10 @@ export interface ElectronAPIs { oldDirPath: string, newDirPath: string ) => Promise; - saveStreamToDisk: (path: string, fileStream: ReadableStream) => void; + saveStreamToDisk: ( + path: string, + fileStream: ReadableStream + ) => Promise; saveFileToDisk: (path: string, file: any) => Promise; selectRootDirectory: () => Promise; sendNotification: (content: string) => void; @@ -63,4 +71,24 @@ export interface ElectronAPIs { getEncryptionKey: () => Promise; openDiskCache: (cacheName: string) => Promise; deleteDiskCache: (cacheName: string) => Promise; + logToDisk: (msg: string) => void; + convertHEIC(fileData: Uint8Array): Promise; + openLogDirectory: () => void; + registerUpdateEventListener: ( + showUpdateDialog: (updateInfo: AppUpdateInfo) => void + ) => void; + updateAndRestart: () => void; + skipAppVersion: (version: string) => void; + getSentryUserID: () => Promise; + getAppVersion: () => Promise; + runFFmpegCmd: ( + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string + ) => Promise; + generateImageThumbnail: ( + inputFile: File | ElectronFile, + maxDimension: number, + maxSize: number + ) => Promise; } diff --git a/src/types/file/index.ts b/src/types/file/index.ts index 1bc7a8d0a..0dc8b3b1b 100644 --- a/src/types/file/index.ts +++ b/src/types/file/index.ts @@ -1,53 +1,71 @@ -import { MagicMetadataCore, VISIBILITY_STATE } from 'types/magicMetadata'; -import { DataStream, Metadata } from 'types/upload'; +import { + EncryptedMagicMetadata, + FileMagicMetadata, + FilePublicMagicMetadata, +} from 'types/magicMetadata'; +import { Metadata } from 'types/upload'; -export interface fileAttribute { - encryptedData?: DataStream | Uint8Array; - objectKey?: string; +interface FileAttributesBase { decryptionHeader: string; } -export interface FileMagicMetadataProps { - visibility?: VISIBILITY_STATE; - filePaths?: string[]; +interface MetadataFileAttributes extends FileAttributesBase { + encryptedData: string; + objectKey?: string; +} +interface S3FileAttributes extends FileAttributesBase { + objectKey: string; + encryptedData?: string; } -export interface FileMagicMetadata extends Omit { - data: FileMagicMetadataProps; +export type FileAttributes = MetadataFileAttributes | S3FileAttributes; + +export interface FileInfo { + fileSize: number; + thumbSize: number; } -export interface FilePublicMagicMetadataProps { - editedTime?: number; - editedName?: string; -} - -export interface FilePublicMagicMetadata - extends Omit { - data: FilePublicMagicMetadataProps; -} - -export interface EnteFile { +export interface EncryptedEnteFile { id: number; collectionID: number; ownerID: number; - file: fileAttribute; - thumbnail: fileAttribute; + file: FileAttributes; + thumbnail: FileAttributes; + metadata: FileAttributes; + info: FileInfo; + magicMetadata: EncryptedMagicMetadata; + pubMagicMetadata: EncryptedMagicMetadata; + encryptedKey: string; + keyDecryptionNonce: string; + isDeleted: boolean; + updationTime: number; +} + +export interface EnteFile + extends Omit< + EncryptedEnteFile, + | 'metadata' + | 'pubMagicMetadata' + | 'magicMetadata' + | 'encryptedKey' + | 'keyDecryptionNonce' + > { metadata: Metadata; magicMetadata: FileMagicMetadata; pubMagicMetadata: FilePublicMagicMetadata; - encryptedKey: string; - keyDecryptionNonce: string; key: string; - src: string; - msrc: string; - html: string; - w: number; - h: number; - isDeleted: boolean; + src?: string; + msrc?: string; + html?: string; + w?: number; + h?: number; + title?: string; isTrashed?: boolean; deleteBy?: number; - dataIndex: number; - updationTime: number; + isSourceLoaded?: boolean; + originalVideoURL?: string; + originalImageURL?: string; + dataIndex?: number; } export interface TrashRequest { diff --git a/src/types/gallery/index.ts b/src/types/gallery/index.ts index 4d6bd7675..4c87cba7c 100644 --- a/src/types/gallery/index.ts +++ b/src/types/gallery/index.ts @@ -4,7 +4,6 @@ import { Collection } from 'types/collection'; import { EnteFile } from 'types/file'; import { Person, ThingClass, WordGroup } from 'types/machineLearning'; import { DateValue, Bbox } from 'types/search'; -import { NotificationAttributes } from 'types/Notification'; export type SelectedState = { [k: number]: boolean; @@ -34,11 +33,10 @@ export type SetCollectionSelectorAttributes = React.Dispatch< export type GalleryContextType = { thumbs: Map; - files: Map; + files: Map; showPlanSelectorModal: () => void; setActiveCollection: (collection: number) => void; syncWithRemote: (force?: boolean, silent?: boolean) => Promise; - setNotificationAttributes: (attributes: NotificationAttributes) => void; setBlockingLoad: (value: boolean) => void; photoListHeader: TimeStampListItem; }; diff --git a/src/types/magicMetadata/index.ts b/src/types/magicMetadata/index.ts index f16d54b9c..14c5dcae9 100644 --- a/src/types/magicMetadata/index.ts +++ b/src/types/magicMetadata/index.ts @@ -5,18 +5,46 @@ export interface MagicMetadataCore { data: Record; } -export interface EncryptedMagicMetadataCore +export interface EncryptedMagicMetadata extends Omit { data: string; } +export interface FileMagicMetadataProps { + visibility?: VISIBILITY_STATE; + filePaths?: string[]; +} + +export interface FileMagicMetadata extends Omit { + data: FileMagicMetadataProps; +} + +export interface FilePublicMagicMetadataProps { + editedTime?: number; + editedName?: string; + caption?: string; + uploaderName?: string; +} + +export interface FilePublicMagicMetadata + extends Omit { + data: FilePublicMagicMetadataProps; +} + export enum VISIBILITY_STATE { - VISIBLE, - ARCHIVED, + VISIBLE = 0, + ARCHIVED = 1, + HIDDEN = 2, +} + +export enum SUB_TYPE { + DEFAULT = 0, + DEFAULT_HIDDEN = 1, + QUICK_LINK_COLLECTION = 2, } export const NEW_FILE_MAGIC_METADATA: MagicMetadataCore = { - version: 0, + version: 1, data: {}, header: null, count: 0, @@ -35,5 +63,5 @@ export interface BulkUpdateMagicMetadataRequest { export interface UpdateMagicMetadataRequest { id: number; - magicMetadata: EncryptedMagicMetadataCore; + magicMetadata: EncryptedMagicMetadata; } diff --git a/src/types/publicCollection/index.ts b/src/types/publicCollection/index.ts index 4b5f3a79b..cef95e129 100644 --- a/src/types/publicCollection/index.ts +++ b/src/types/publicCollection/index.ts @@ -1,5 +1,6 @@ import { TimeStampListItem } from 'components/PhotoList'; import { REPORT_REASON } from 'constants/publicCollection'; +import { PublicURL } from 'types/collection'; import { EnteFile } from 'types/file'; export interface PublicCollectionGalleryContextType { @@ -8,6 +9,7 @@ export interface PublicCollectionGalleryContextType { accessedThroughSharedURL: boolean; openReportForm: () => void; photoListHeader: TimeStampListItem; + photoListFooter: TimeStampListItem; } export interface LocalSavedPublicCollectionFiles { @@ -39,3 +41,7 @@ export interface Address { postalCode: string; phone: string; } + +export type SetPublicShareProp = React.Dispatch< + React.SetStateAction +>; diff --git a/src/types/trash/index.ts b/src/types/trash/index.ts index 9a9492ce6..48de52a21 100644 --- a/src/types/trash/index.ts +++ b/src/types/trash/index.ts @@ -1,11 +1,16 @@ -import { EnteFile } from 'types/file'; +import { EncryptedEnteFile, EnteFile } from 'types/file'; -export interface TrashItem { +export interface TrashItem extends Omit { file: EnteFile; +} + +export interface EncryptedTrashItem { + file: EncryptedEnteFile; isDeleted: boolean; isRestored: boolean; deleteBy: number; createdAt: number; updatedAt: number; } + export type Trash = TrashItem[]; diff --git a/src/types/upload/index.ts b/src/types/upload/index.ts index 871e03e0a..2a4f71302 100644 --- a/src/types/upload/index.ts +++ b/src/types/upload/index.ts @@ -1,6 +1,11 @@ import { FILE_TYPE } from 'constants/file'; import { Collection } from 'types/collection'; -import { fileAttribute } from 'types/file'; +import { B64EncryptionResult, LocalFileAttributes } from 'types/crypto'; +import { FileAttributes } from 'types/file'; +import { + EncryptedMagicMetadata, + FilePublicMagicMetadata, +} from 'types/magicMetadata'; export interface DataStream { stream: ReadableStream; @@ -11,11 +16,6 @@ export function isDataStream(object: any): object is DataStream { return 'stream' in object; } -export interface EncryptionResult { - file: fileAttribute; - key: string; -} - export interface Metadata { title: string; creationTime: number; @@ -89,13 +89,7 @@ export interface FileWithCollection extends UploadAsset { collection?: Collection; collectionID?: number; } -export interface MetadataAndFileTypeInfo { - metadata: Metadata; - fileTypeInfo: FileTypeInfo; - filePath: string; -} -export type MetadataAndFileTypeInfoMap = Map; export type ParsedMetadataJSONMap = Map; export interface UploadURL { @@ -103,12 +97,6 @@ export interface UploadURL { objectKey: string; } -export interface B64EncryptionResult { - encryptedData: string; - key: string; - nonce: string; -} - export interface FileInMemory { filedata: Uint8Array | DataStream; thumbnail: Uint8Array; @@ -119,6 +107,7 @@ export interface FileWithMetadata extends Omit { metadata: Metadata; localID: number; + pubMagicMetadata: FilePublicMagicMetadata; } export interface EncryptedFile { @@ -126,12 +115,18 @@ export interface EncryptedFile { fileKey: B64EncryptionResult; } export interface ProcessedFile { - file: fileAttribute; - thumbnail: fileAttribute; - metadata: fileAttribute; + file: LocalFileAttributes; + thumbnail: LocalFileAttributes; + metadata: LocalFileAttributes; + pubMagicMetadata: EncryptedMagicMetadata; localID: number; } -export interface BackupedFile extends Omit {} +export interface BackupedFile { + file: FileAttributes; + thumbnail: FileAttributes; + metadata: FileAttributes; + pubMagicMetadata: EncryptedMagicMetadata; +} export interface UploadFile extends BackupedFile { collectionID: number; @@ -148,4 +143,11 @@ export interface ParsedExtractedMetadata { export interface ImportSuggestion { rootFolderName: string; hasNestedFolders: boolean; + hasRootLevelFileWithFolder: boolean; +} + +export interface PublicUploadProps { + token: string; + passwordToken: string; + accessedThroughSharedURL: boolean; } diff --git a/src/utils/billing/index.ts b/src/utils/billing/index.ts index 2394eb487..b56a119bf 100644 --- a/src/utils/billing/index.ts +++ b/src/utils/billing/index.ts @@ -41,7 +41,7 @@ export function makeHumanReadableStorage( bytes: number, round: 'round-up' | 'round-down' = 'round-down' ): string { - if (bytes === 0) { + if (bytes <= 0) { return '0 MB'; } const i = Math.floor(Math.log(bytes) / Math.log(1024)); @@ -153,7 +153,6 @@ export function isUserSubscribedPlan(plan: Plan, subscription: Subscription) { } export function hasStripeSubscription(subscription: Subscription) { return ( - hasPaidSubscription(subscription) && subscription.paymentProvider.length > 0 && subscription.paymentProvider === PAYMENT_PROVIDER_STRIPE ); diff --git a/src/utils/collection/index.ts b/src/utils/collection/index.ts index 1743e2c91..812c3641e 100644 --- a/src/utils/collection/index.ts +++ b/src/utils/collection/index.ts @@ -26,13 +26,15 @@ import { SYSTEM_COLLECTION_TYPES, UPLOAD_NOT_ALLOWED_COLLECTION_TYPES, } from 'constants/collection'; -import { getAlbumSiteHost } from 'constants/pages'; import { getUnixTimeInMicroSecondsWithDelta } from 'utils/time'; import { NEW_COLLECTION_MAGIC_METADATA, + SUB_TYPE, VISIBILITY_STATE, } from 'types/magicMetadata'; import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata'; +import { getAlbumsURL } from 'utils/common/apiUtil'; +import bs58 from 'bs58'; export enum COLLECTION_OPS_TYPE { ADD, @@ -119,12 +121,14 @@ export function appendCollectionKeyToShareURL( if (!url) { return null; } - const bs58 = require('bs58'); + const sharableURL = new URL(url); - if (process.env.NODE_ENV === 'development') { - sharableURL.host = getAlbumSiteHost(); - sharableURL.protocol = 'http'; - } + const albumsURL = new URL(getAlbumsURL()); + + sharableURL.protocol = albumsURL.protocol; + sharableURL.host = albumsURL.host; + sharableURL.pathname = albumsURL.pathname; + const bytes = Buffer.from(collectionKey, 'base64'); sharableURL.hash = bs58.encode(bytes); return sharableURL.href; @@ -226,3 +230,14 @@ export const getUserOwnedCollections = (collections: Collection[]) => { } return collections.filter((collection) => collection.owner.id === user.id); }; + +export const getNonHiddenCollections = (collections: Collection[]) => { + return collections.filter((collection) => !isCollectionHidden(collection)); +}; + +export const isCollectionHidden = (collection: Collection) => + collection.magicMetadata?.data.visibility === VISIBILITY_STATE.HIDDEN || + collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN; + +export const isQuickLinkCollection = (collection: Collection) => + collection.magicMetadata?.data.subType === SUB_TYPE.QUICK_LINK_COLLECTION; diff --git a/src/utils/comlink/ComlinkConvertWorker.ts b/src/utils/comlink/ComlinkConvertWorker.ts new file mode 100644 index 000000000..cde43a601 --- /dev/null +++ b/src/utils/comlink/ComlinkConvertWorker.ts @@ -0,0 +1,30 @@ +import { Remote } from 'comlink'; +import { runningInBrowser } from 'utils/common'; +import { DedicatedConvertWorker } from 'worker/convert.worker'; +import { ComlinkWorker } from './comlinkWorker'; + +class ComlinkConvertWorker { + private comlinkWorkerInstance: Remote; + + async getInstance() { + if (!this.comlinkWorkerInstance) { + const comlinkWorker = getDedicatedConvertWorker(); + this.comlinkWorkerInstance = await new comlinkWorker.remote(); + } + return this.comlinkWorkerInstance; + } +} + +export const getDedicatedConvertWorker = () => { + if (runningInBrowser()) { + const cryptoComlinkWorker = new ComlinkWorker< + typeof DedicatedConvertWorker + >( + 'ente-convert-worker', + new Worker(new URL('worker/convert.worker.ts', import.meta.url)) + ); + return cryptoComlinkWorker; + } +}; + +export default new ComlinkConvertWorker(); diff --git a/src/utils/comlink/ComlinkCryptoWorker.ts b/src/utils/comlink/ComlinkCryptoWorker.ts new file mode 100644 index 000000000..bc00da94c --- /dev/null +++ b/src/utils/comlink/ComlinkCryptoWorker.ts @@ -0,0 +1,25 @@ +import { Remote } from 'comlink'; +import { DedicatedCryptoWorker } from 'worker/crypto.worker'; +import { ComlinkWorker } from './comlinkWorker'; + +class ComlinkCryptoWorker { + private comlinkWorkerInstance: Remote; + + async getInstance() { + if (!this.comlinkWorkerInstance) { + const comlinkWorker = getDedicatedCryptoWorker(); + this.comlinkWorkerInstance = await new comlinkWorker.remote(); + } + return this.comlinkWorkerInstance; + } +} + +export const getDedicatedCryptoWorker = () => { + const cryptoComlinkWorker = new ComlinkWorker( + 'ente-crypto-worker', + new Worker(new URL('worker/crypto.worker.ts', import.meta.url)) + ); + return cryptoComlinkWorker; +}; + +export default new ComlinkCryptoWorker(); diff --git a/src/utils/comlink/ComlinkFFmpegWorker.ts b/src/utils/comlink/ComlinkFFmpegWorker.ts new file mode 100644 index 000000000..3fb5dba7b --- /dev/null +++ b/src/utils/comlink/ComlinkFFmpegWorker.ts @@ -0,0 +1,25 @@ +import { Remote } from 'comlink'; +import { DedicatedFFmpegWorker } from 'worker/ffmpeg.worker'; +import { ComlinkWorker } from './comlinkWorker'; + +class ComlinkFFmpegWorker { + private comlinkWorkerInstance: Remote; + + async getInstance() { + if (!this.comlinkWorkerInstance) { + const comlinkWorker = getDedicatedFFmpegWorker(); + this.comlinkWorkerInstance = await new comlinkWorker.remote(); + } + return this.comlinkWorkerInstance; + } +} + +const getDedicatedFFmpegWorker = () => { + const cryptoComlinkWorker = new ComlinkWorker( + 'ente-ffmpeg-worker', + new Worker(new URL('worker/ffmpeg.worker.ts', import.meta.url)) + ); + return cryptoComlinkWorker; +}; + +export default new ComlinkFFmpegWorker(); diff --git a/src/utils/comlink/comlinkWorker.ts b/src/utils/comlink/comlinkWorker.ts new file mode 100644 index 000000000..009234d74 --- /dev/null +++ b/src/utils/comlink/comlinkWorker.ts @@ -0,0 +1,24 @@ +import { Remote, wrap } from 'comlink'; +import { addLocalLog } from 'utils/logging'; + +export class ComlinkWorker { + public remote: Remote; + private worker: Worker; + private name: string; + + constructor(name: string, worker: Worker) { + this.name = name; + this.worker = worker; + + this.worker.onerror = (errorEvent) => { + console.error('Got error event from worker', errorEvent); + }; + addLocalLog(() => `Initiated ${this.name}`); + this.remote = wrap(this.worker); + } + + public terminate() { + this.worker.terminate(); + addLocalLog(() => `Terminated ${this.name}`); + } +} diff --git a/src/utils/comlink/index.ts b/src/utils/comlink/index.ts deleted file mode 100644 index fbc9c487d..000000000 --- a/src/utils/comlink/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as Comlink from 'comlink'; -import { runningInBrowser } from 'utils/common'; - -export interface ComlinkWorker { - comlink: any; - worker: Worker; -} - -const getDedicatedFFmpegWorker = (): ComlinkWorker => { - if (runningInBrowser()) { - const worker = new Worker( - new URL('worker/ffmpeg.worker.js', import.meta.url), - { name: 'ente-ffmpeg-worker' } - ); - const comlink = Comlink.wrap(worker); - return { comlink, worker }; - } -}; - -export const FFmpegWorker: any = getDedicatedFFmpegWorker()?.comlink; diff --git a/src/utils/common/apiUtil.ts b/src/utils/common/apiUtil.ts index d89ae003b..b3b5a2935 100644 --- a/src/utils/common/apiUtil.ts +++ b/src/utils/common/apiUtil.ts @@ -66,6 +66,14 @@ export const getFamilyPortalURL = () => { return `https://family.ente.io`; }; +export const getAlbumsURL = () => { + const albumsURL = process.env.NEXT_PUBLIC_ENTE_ALBUM_ENDPOINT; + if (isDevDeployment() && albumsURL) { + return albumsURL; + } + return `https://albums.ente.io`; +}; + export const getSentryTunnelURL = () => { return `https://sentry-reporter.ente.io`; }; @@ -80,9 +88,9 @@ const isDevDeployment = () => { if (runningInBrowser()) { return ( process.env.NEXT_PUBLIC_ENTE_WEB_ENDPOINT === - globalThis.location.origin || + window.location.origin || process.env.NEXT_PUBLIC_ENTE_ALBUM_ENDPOINT === - globalThis.location.origin || + window.location.origin || process.env.NODE_ENV === 'development' ); } diff --git a/src/utils/common/deviceDetection.ts b/src/utils/common/deviceDetection.ts index e6855d91e..e9aca75d7 100644 --- a/src/utils/common/deviceDetection.ts +++ b/src/utils/common/deviceDetection.ts @@ -15,7 +15,7 @@ declare global { } } -const GetDeviceOS = () => { +export const getDeviceOS = () => { let userAgent = ''; if ( typeof window !== 'undefined' && @@ -52,4 +52,18 @@ const GetDeviceOS = () => { return OS.UNKNOWN; }; -export default GetDeviceOS; +export const isMobileOrTable = () => { + let check = false; + (function (a) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( + a + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( + a.substr(0, 4) + ) + ) + check = true; + })(navigator.userAgent || navigator.vendor || window.opera); + return check; +}; diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index 5fc9e170a..2da2a0792 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -1,19 +1,7 @@ -import constants from 'utils/strings/constants'; import { CustomError } from 'utils/error'; -import GetDeviceOS, { OS } from './deviceDetection'; import isElectron from 'is-electron'; -const DESKTOP_APP_GITHUB_DOWNLOAD_URL = - 'https://github.com/ente-io/bhari-frame/releases/latest'; - -const APP_DOWNLOAD_ENTE_URL_PREFIX = 'https://ente.io/download'; - -export function checkConnectivity() { - if (navigator.onLine) { - return true; - } - throw new Error(constants.NO_INTERNET_CONNECTION); -} +export const APP_DOWNLOAD_URL = 'https://ente.io/download/desktop'; export function runningInBrowser() { return typeof window !== 'undefined'; @@ -61,22 +49,8 @@ export async function sleep(time: number) { }); } -export function getOSSpecificDesktopAppDownloadLink() { - const os = GetDeviceOS(); - let url = ''; - if (os === OS.WINDOWS) { - url = `${APP_DOWNLOAD_ENTE_URL_PREFIX}/exe`; - } else if (os === OS.MAC) { - url = `${APP_DOWNLOAD_ENTE_URL_PREFIX}/dmg`; - } else { - url = DESKTOP_APP_GITHUB_DOWNLOAD_URL; - } - return url; -} export function downloadApp() { - const link = getOSSpecificDesktopAppDownloadLink(); - const win = window.open(link, '_blank'); - win.focus(); + openLink(APP_DOWNLOAD_URL, true); } export function reverseString(title: string) { @@ -91,12 +65,12 @@ export function initiateEmail(email: string) { a.rel = 'noreferrer noopener'; a.click(); } -export const promiseWithTimeout = async ( - request: Promise, +export const promiseWithTimeout = async ( + request: Promise, timeout: number -) => { +): Promise => { const timeoutRef = { current: null }; - const rejectOnTimeout = new Promise((_, reject) => { + const rejectOnTimeout = new Promise((_, reject) => { timeoutRef.current = setTimeout( () => reject(Error(CustomError.WAIT_TIME_EXCEEDED)), timeout @@ -147,3 +121,7 @@ function isPromise(p: any) { return false; } + +export function isClipboardItemPresent() { + return typeof ClipboardItem !== 'undefined'; +} diff --git a/src/utils/common/key.ts b/src/utils/common/key.ts index 58309210e..48df6a137 100644 --- a/src/utils/common/key.ts +++ b/src/utils/common/key.ts @@ -1,5 +1,5 @@ -import { B64EncryptionResult } from 'utils/crypto'; -import CryptoWorker from 'utils/crypto'; +import { B64EncryptionResult } from 'types/crypto'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { CustomError } from '../error'; @@ -10,8 +10,8 @@ export const getActualKey = async () => { SESSION_KEYS.ENCRYPTION_KEY ); - const cryptoWorker = await new CryptoWorker(); - const key: string = await cryptoWorker.decryptB64( + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const key = await cryptoWorker.decryptB64( encryptionKeyAttributes.encryptedData, encryptionKeyAttributes.nonce, encryptionKeyAttributes.key diff --git a/src/utils/crypto/index.ts b/src/utils/crypto/index.ts index f1511ec54..99ac12dec 100644 --- a/src/utils/crypto/index.ts +++ b/src/utils/crypto/index.ts @@ -1,52 +1,40 @@ -import { KEK, KeyAttributes } from 'types/user'; -import * as Comlink from 'comlink'; -import { runningInBrowser } from 'utils/common'; +import { KeyAttributes } from 'types/user'; import { SESSION_KEYS, setKey } from 'utils/storage/sessionStorage'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; import { getActualKey, getToken } from 'utils/common/key'; import { setRecoveryKey } from 'services/userService'; import { logError } from 'utils/sentry'; -import { ComlinkWorker } from 'utils/comlink'; import isElectron from 'is-electron'; import safeStorageService from 'services/electron/safeStorage'; - -export interface B64EncryptionResult { - encryptedData: string; - key: string; - nonce: string; -} - -export const getDedicatedCryptoWorker = (): ComlinkWorker => { - if (runningInBrowser()) { - const worker = new Worker( - new URL('worker/crypto.worker.js', import.meta.url), - { name: 'ente-crypto-worker' } - ); - const comlink = Comlink.wrap(worker); - return { comlink, worker }; - } -}; -const CryptoWorker: any = getDedicatedCryptoWorker()?.comlink; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; export async function generateKeyAttributes( passphrase: string ): Promise<{ keyAttributes: KeyAttributes; masterKey: string }> { - const cryptoWorker = await new CryptoWorker(); - const masterKey: string = await cryptoWorker.generateEncryptionKey(); - const recoveryKey: string = await cryptoWorker.generateEncryptionKey(); - const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const masterKey = await cryptoWorker.generateEncryptionKey(); + const recoveryKey = await cryptoWorker.generateEncryptionKey(); + const kekSalt = await cryptoWorker.generateSaltToDeriveKey(); const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt); - const masterKeyEncryptedWithKek: B64EncryptionResult = - await cryptoWorker.encryptToB64(masterKey, kek.key); - const masterKeyEncryptedWithRecoveryKey: B64EncryptionResult = - await cryptoWorker.encryptToB64(masterKey, recoveryKey); - const recoveryKeyEncryptedWithMasterKey: B64EncryptionResult = - await cryptoWorker.encryptToB64(recoveryKey, masterKey); + const masterKeyEncryptedWithKek = await cryptoWorker.encryptToB64( + masterKey, + kek.key + ); + const masterKeyEncryptedWithRecoveryKey = await cryptoWorker.encryptToB64( + masterKey, + recoveryKey + ); + const recoveryKeyEncryptedWithMasterKey = await cryptoWorker.encryptToB64( + recoveryKey, + masterKey + ); const keyPair = await cryptoWorker.generateKeyPair(); - const encryptedKeyPairAttributes: B64EncryptionResult = - await cryptoWorker.encryptToB64(keyPair.privateKey, masterKey); + const encryptedKeyPairAttributes = await cryptoWorker.encryptToB64( + keyPair.privateKey, + masterKey + ); const keyAttributes: KeyAttributes = { kekSalt, @@ -69,19 +57,20 @@ export async function generateKeyAttributes( } export async function generateAndSaveIntermediateKeyAttributes( - passphrase, - existingKeyAttributes, - key + passphrase: string, + existingKeyAttributes: KeyAttributes, + key: string ): Promise { - const cryptoWorker = await new CryptoWorker(); - const intermediateKekSalt: string = - await cryptoWorker.generateSaltToDeriveKey(); - const intermediateKek: KEK = await cryptoWorker.deriveInteractiveKey( + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const intermediateKekSalt = await cryptoWorker.generateSaltToDeriveKey(); + const intermediateKek = await cryptoWorker.deriveInteractiveKey( passphrase, intermediateKekSalt ); - const encryptedKeyAttributes: B64EncryptionResult = - await cryptoWorker.encryptToB64(key, intermediateKek.key); + const encryptedKeyAttributes = await cryptoWorker.encryptToB64( + key, + intermediateKek.key + ); const intermediateKeyAttributes = Object.assign(existingKeyAttributes, { kekSalt: intermediateKekSalt, @@ -99,7 +88,7 @@ export const saveKeyInSessionStore = async ( key: string, fromDesktop?: boolean ) => { - const cryptoWorker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const sessionKeyAttributes = await cryptoWorker.encryptToB64(key); setKey(keyType, sessionKeyAttributes); if (isElectron() && !fromDesktop) { @@ -110,7 +99,7 @@ export const saveKeyInSessionStore = async ( export const getRecoveryKey = async () => { let recoveryKey: string = null; try { - const cryptoWorker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); const { @@ -139,13 +128,17 @@ async function createNewRecoveryKey() { const masterKey = await getActualKey(); const existingAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); - const cryptoWorker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const recoveryKey: string = await cryptoWorker.generateEncryptionKey(); - const encryptedMasterKey: B64EncryptionResult = - await cryptoWorker.encryptToB64(masterKey, recoveryKey); - const encryptedRecoveryKey: B64EncryptionResult = - await cryptoWorker.encryptToB64(recoveryKey, masterKey); + const recoveryKey = await cryptoWorker.generateEncryptionKey(); + const encryptedMasterKey = await cryptoWorker.encryptToB64( + masterKey, + recoveryKey + ); + const encryptedRecoveryKey = await cryptoWorker.encryptToB64( + recoveryKey, + masterKey + ); const recoveryKeyAttributes = { masterKeyEncryptedWithRecoveryKey: encryptedMasterKey.encryptedData, masterKeyDecryptionNonce: encryptedMasterKey.nonce, @@ -163,7 +156,7 @@ async function createNewRecoveryKey() { return recoveryKey; } export async function decryptAndStoreToken(masterKey: string) { - const cryptoWorker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const user = getData(LS_KEYS.USER); const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); let decryptedToken = null; @@ -192,20 +185,17 @@ export async function decryptAndStoreToken(masterKey: string) { } export async function encryptWithRecoveryKey(key: string) { - const cryptoWorker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const hexRecoveryKey = await getRecoveryKey(); const recoveryKey = await cryptoWorker.fromHex(hexRecoveryKey); - const encryptedKey: B64EncryptionResult = await cryptoWorker.encryptToB64( - key, - recoveryKey - ); + const encryptedKey = await cryptoWorker.encryptToB64(key, recoveryKey); return encryptedKey; } export async function decryptDeleteAccountChallenge( encryptedChallenge: string ) { - const cryptoWorker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); const masterKey = await getActualKey(); const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); const secretKey = await cryptoWorker.decryptB64( @@ -221,4 +211,3 @@ export async function decryptDeleteAccountChallenge( const utf8DecryptedChallenge = atob(b64DecryptedChallenge); return utf8DecryptedChallenge; } -export default CryptoWorker; diff --git a/src/utils/crypto/libsodium.ts b/src/utils/crypto/libsodium.ts index 90138ab48..e942c1c7b 100644 --- a/src/utils/crypto/libsodium.ts +++ b/src/utils/crypto/libsodium.ts @@ -1,6 +1,7 @@ import sodium, { StateAddress } from 'libsodium-wrappers'; import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto'; import assert from 'assert'; +import { B64EncryptionResult } from 'types/crypto'; export async function decryptChaChaOneShot( data: Uint8Array, @@ -188,7 +189,7 @@ export async function encryptToB64(data: string, key?: string) { encryptedData: await toB64(encrypted.encryptedData), key: await toB64(encrypted.key), nonce: await toB64(encrypted.nonce), - }; + } as B64EncryptionResult; } export async function encryptUTF8(data: string, key?: string) { const b64Data = await toB64(await fromString(data)); diff --git a/src/utils/error/index.ts b/src/utils/error/index.ts index 5a02dbbd3..611f4acb0 100644 --- a/src/utils/error/index.ts +++ b/src/utils/error/index.ts @@ -1,5 +1,4 @@ import { AxiosResponse } from 'axios'; -import constants from 'utils/strings/constants'; export const ServerErrorCodes = { SESSION_EXPIRED: '401', @@ -15,7 +14,6 @@ export const ServerErrorCodes = { }; export enum CustomError { - UNKNOWN_ERROR = 'unknown error', SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed', THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed', VIDEO_PLAYBACK_FAILED = 'video playback failed', @@ -23,6 +21,7 @@ export enum CustomError { KEY_MISSING = 'encrypted key missing from localStorage', FAILED_TO_LOAD_WEB_WORKER = 'failed to load web worker', CHUNK_MORE_THAN_EXPECTED = 'chunks more than expected', + CHUNK_LESS_THAN_EXPECTED = 'chunks less than expected', UNSUPPORTED_FILE_FORMAT = 'unsupported file formats', FILE_TOO_LARGE = 'file too large', SUBSCRIPTION_EXPIRED = 'subscription expired', @@ -37,6 +36,7 @@ export enum CustomError { NETWORK_ERROR = 'Network Error', REQUEST_FAILED = 'request failed', TOKEN_EXPIRED = 'token expired', + TOKEN_MISSING = 'token missing', TOO_MANY_REQUESTS = 'too many requests', BAD_REQUEST = 'bad request', SUBSCRIPTION_NEEDED = 'subscription not present', @@ -49,6 +49,8 @@ export enum CustomError { INCORRECT_PASSWORD = 'incorrect password', UPLOAD_CANCELLED = 'upload cancelled', REQUEST_TIMEOUT = 'request taking too long', + HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED = 'hidden collection sync file attempted', + UNKNOWN_ERROR = 'Something went wrong, please try again', } export function parseServerError(error: AxiosResponse): string { @@ -68,7 +70,7 @@ export function parseServerError(error: AxiosResponse): string { parsedMessage = CustomError.FILE_TOO_LARGE; break; default: - parsedMessage = `${constants.UNKNOWN_ERROR} statusCode:${errorCode}`; + parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${errorCode}`; } return parsedMessage; @@ -92,7 +94,7 @@ function parseUploadErrorCodes(error) { parsedMessage = CustomError.FILE_TOO_LARGE; break; default: - parsedMessage = `${constants.UNKNOWN_ERROR} statusCode:${errorCode}`; + parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${errorCode}`; } } else { parsedMessage = error.message; @@ -145,29 +147,10 @@ export const parseSharingErrorCodes = (error) => { parsedMessage = CustomError.TOO_MANY_REQUESTS; break; default: - parsedMessage = `${constants.UNKNOWN_ERROR} statusCode:${errorCode}`; + parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${errorCode}`; } } else { parsedMessage = error.message; } return new Error(parsedMessage); }; - -export const handleSharingErrors = (error) => { - const parsedError = parseSharingErrorCodes(error); - let errorMessage = ''; - switch (parsedError.message) { - case CustomError.BAD_REQUEST: - errorMessage = constants.SHARING_BAD_REQUEST_ERROR; - break; - case CustomError.SUBSCRIPTION_NEEDED: - errorMessage = constants.SHARING_DISABLED_FOR_FREE_ACCOUNTS; - break; - case CustomError.NOT_FOUND: - errorMessage = constants.USER_DOES_NOT_EXIST; - break; - default: - errorMessage = parsedError.message; - } - return errorMessage; -}; diff --git a/src/utils/error/ui.ts b/src/utils/error/ui.ts new file mode 100644 index 000000000..fdf695dff --- /dev/null +++ b/src/utils/error/ui.ts @@ -0,0 +1,28 @@ +import constants from 'utils/strings/constants'; +import { parseSharingErrorCodes, CustomError } from '.'; + +export const handleSharingErrors = (error) => { + const parsedError = parseSharingErrorCodes(error); + let errorMessage = ''; + switch (parsedError.message) { + case CustomError.BAD_REQUEST: + errorMessage = constants.SHARING_BAD_REQUEST_ERROR; + break; + case CustomError.SUBSCRIPTION_NEEDED: + errorMessage = constants.SHARING_DISABLED_FOR_FREE_ACCOUNTS; + break; + case CustomError.NOT_FOUND: + errorMessage = constants.USER_DOES_NOT_EXIST; + break; + default: + errorMessage = parsedError.message; + } + return errorMessage; +}; + +export function checkConnectivity() { + if (navigator.onLine) { + return true; + } + throw new Error(constants.NO_INTERNET_CONNECTION); +} diff --git a/src/utils/exif/index.ts b/src/utils/exif/index.ts deleted file mode 100644 index 83e1a6695..000000000 --- a/src/utils/exif/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function prettyPrintExif(exifData: Object) { - let strPretty = ''; - for (const [tagName, tagValue] of Object.entries(exifData)) { - if (tagValue instanceof Uint8Array) { - strPretty += tagName + ' : ' + '[' + tagValue + ']' + '\r\n'; - } else if (tagValue instanceof Date) { - strPretty += tagName + ' : ' + tagValue.toDateString() + '\r\n'; - } else { - strPretty += tagName + ' : ' + tagValue + '\r\n'; - } - } - return strPretty; -} diff --git a/src/utils/export/index.ts b/src/utils/export/index.ts index 7b40b2ce1..1d2010f3c 100644 --- a/src/utils/export/index.ts +++ b/src/utils/export/index.ts @@ -5,8 +5,10 @@ import { CollectionIDPathMap, ExportRecord } from 'types/export'; import { EnteFile } from 'types/file'; import { Metadata } from 'types/upload'; -import { formatDate, splitFilenameAndExtension } from 'utils/file'; +import { splitFilenameAndExtension } from 'utils/file'; import { ENTE_METADATA_FOLDER } from 'constants/export'; +import sanitize from 'sanitize-filename'; +import { formatDateTimeShort } from 'utils/time/format'; export const getExportRecordFileUID = (file: EnteFile) => `${file.id}_${file.collectionID}_${file.updationTime}`; @@ -127,22 +129,25 @@ export const dedupe = (files: any[]) => { export const getGoogleLikeMetadataFile = ( fileSaveName: string, - metadata: Metadata + file: EnteFile ) => { + const metadata: Metadata = file.metadata; const creationTime = Math.floor(metadata.creationTime / 1000000); const modificationTime = Math.floor( (metadata.modificationTime ?? metadata.creationTime) / 1000000 ); + const captionValue: string = file?.pubMagicMetadata?.data?.caption; return JSON.stringify( { title: fileSaveName, + caption: captionValue, creationTime: { timestamp: creationTime, - formatted: formatDate(creationTime * 1000), + formatted: formatDateTimeShort(creationTime * 1000), }, modificationTime: { timestamp: modificationTime, - formatted: formatDate(modificationTime * 1000), + formatted: formatDateTimeShort(modificationTime * 1000), }, geoData: { latitude: metadata.latitude, @@ -158,7 +163,7 @@ export const oldSanitizeName = (name: string) => name.replaceAll('/', '_').replaceAll(' ', '_'); export const sanitizeName = (name: string) => - name.replace(/[^a-z0-9.]/gi, '_').toLowerCase(); + sanitize(name, { replacement: '_' }); export const getUniqueCollectionFolderPath = ( dir: string, diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index ae4fed4ef..bdb76ed75 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -1,16 +1,10 @@ import { SelectedState } from 'types/gallery'; -import { - EnteFile, - fileAttribute, - FileMagicMetadataProps, - FilePublicMagicMetadataProps, -} from 'types/file'; +import { EnteFile, EncryptedEnteFile } from 'types/file'; import { decodeMotionPhoto } from 'services/motionPhotoService'; import { getFileType } from 'services/typeDetectionService'; import DownloadManager from 'services/downloadManager'; import { logError } from 'utils/sentry'; import { User } from 'types/user'; -import CryptoWorker from 'utils/crypto'; import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { updateFileCreationDateInEXIF } from 'services/upload/exifService'; import { @@ -21,27 +15,31 @@ import { FILE_TYPE, } from 'constants/file'; import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager'; -import HEICConverter from 'services/heicConverter/heicConverterService'; -import ffmpegService from 'services/ffmpeg/ffmpegService'; -import { NEW_FILE_MAGIC_METADATA, VISIBILITY_STATE } from 'types/magicMetadata'; +import heicConversionService from 'services/heicConversionService'; +import * as ffmpegService from 'services/ffmpeg/ffmpegService'; +import { + FileMagicMetadata, + FileMagicMetadataProps, + FilePublicMagicMetadata, + FilePublicMagicMetadataProps, + NEW_FILE_MAGIC_METADATA, + VISIBILITY_STATE, +} from 'types/magicMetadata'; import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata'; import { addLogLine } from 'utils/logging'; +import { CustomError } from 'utils/error'; import { convertBytesToHumanReadable } from './size'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; + +const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000; + export function downloadAsFile(filename: string, content: string) { const file = new Blob([content], { type: 'text/plain', }); - const a = document.createElement('a'); - a.href = URL.createObjectURL(file); - a.download = filename; - - a.style.display = 'none'; - document.body.appendChild(a); - - a.click(); - - a.remove(); + const fileURL = URL.createObjectURL(file); + downloadUsingAnchor(fileURL, filename); } export async function downloadFile( @@ -115,10 +113,6 @@ export async function downloadFile( tempURL = URL.createObjectURL(fileBlob); downloadUsingAnchor(tempURL, file.metadata.title); } - - tempURL && URL.revokeObjectURL(tempURL); - tempImageURL && URL.revokeObjectURL(tempImageURL); - tempVideoURL && URL.revokeObjectURL(tempVideoURL); } function downloadUsingAnchor(link: string, name: string) { @@ -128,6 +122,7 @@ function downloadUsingAnchor(link: string, name: string) { a.download = name; document.body.appendChild(a); a.click(); + URL.revokeObjectURL(link); a.remove(); } @@ -169,16 +164,6 @@ export function getSelectedFiles( return selectedFiles; } -export function formatDate(date: number | Date) { - const dateTimeFormat = new Intl.DateTimeFormat('en-IN', { - weekday: 'short', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - return dateTimeFormat.format(date); -} - export function sortFiles(files: EnteFile[]) { // sort according to modification time first files = files.sort((a, b) => { @@ -207,41 +192,59 @@ export function sortFiles(files: EnteFile[]) { return files; } -export async function decryptFile(file: EnteFile, collectionKey: string) { +export async function decryptFile( + file: EncryptedEnteFile, + collectionKey: string +): Promise { try { - const worker = await new CryptoWorker(); - file.key = await worker.decryptB64( - file.encryptedKey, - file.keyDecryptionNonce, + const worker = await ComlinkCryptoWorker.getInstance(); + const { + encryptedKey, + keyDecryptionNonce, + metadata, + magicMetadata, + pubMagicMetadata, + ...restFileProps + } = file; + const fileKey = await worker.decryptB64( + encryptedKey, + keyDecryptionNonce, collectionKey ); - const encryptedMetadata = file.metadata as unknown as fileAttribute; - file.metadata = await worker.decryptMetadata( - encryptedMetadata.encryptedData, - encryptedMetadata.decryptionHeader, - file.key + const fileMetadata = await worker.decryptMetadata( + metadata.encryptedData, + metadata.decryptionHeader, + fileKey ); - if ( - file.magicMetadata?.data && - typeof file.magicMetadata.data === 'string' - ) { - file.magicMetadata.data = await worker.decryptMetadata( - file.magicMetadata.data, - file.magicMetadata.header, - file.key - ); + let fileMagicMetadata: FileMagicMetadata; + let filePubMagicMetadata: FilePublicMagicMetadata; + if (magicMetadata?.data) { + fileMagicMetadata = { + ...file.magicMetadata, + data: await worker.decryptMetadata( + magicMetadata.data, + magicMetadata.header, + fileKey + ), + }; } - if ( - file.pubMagicMetadata?.data && - typeof file.pubMagicMetadata.data === 'string' - ) { - file.pubMagicMetadata.data = await worker.decryptMetadata( - file.pubMagicMetadata.data, - file.pubMagicMetadata.header, - file.key - ); + if (pubMagicMetadata?.data) { + filePubMagicMetadata = { + ...pubMagicMetadata, + data: await worker.decryptMetadata( + pubMagicMetadata.data, + pubMagicMetadata.header, + fileKey + ), + }; } - return file; + return { + ...restFileProps, + key: fileKey, + metadata: fileMetadata, + magicMetadata: fileMagicMetadata, + pubMagicMetadata: filePubMagicMetadata, + }; } catch (e) { logError(e, 'file decryption failed'); throw e; @@ -303,14 +306,25 @@ export async function getRenderableFileURL(file: EnteFile, fileBlob: Blob) { file.metadata.title, fileBlob ); - return [URL.createObjectURL(convertedBlob)]; + return { + converted: [URL.createObjectURL(convertedBlob)], + original: [URL.createObjectURL(fileBlob)], + }; } case FILE_TYPE.LIVE_PHOTO: { const livePhoto = await getRenderableLivePhoto(file, fileBlob); - return livePhoto.map((asset) => URL.createObjectURL(asset)); + return { + converted: livePhoto.map((asset) => URL.createObjectURL(asset)), + original: [URL.createObjectURL(fileBlob)], + }; + } + default: { + const previewURL = URL.createObjectURL(fileBlob); + return { + converted: [previewURL], + original: [previewURL], + }; } - default: - return [URL.createObjectURL(fileBlob)]; } } @@ -328,10 +342,9 @@ async function getRenderableLivePhoto( async function getPlayableVideo(videoNameTitle: string, video: Uint8Array) { const mp4ConvertedVideo = await ffmpegService.convertToMP4( - video, - videoNameTitle + new File([video], videoNameTitle) ); - return new Blob([mp4ConvertedVideo]); + return new Blob([await mp4ConvertedVideo.arrayBuffer()]); } export async function getRenderableImage(fileName: string, imageBlob: Blob) { @@ -341,7 +354,10 @@ export async function getRenderableImage(fileName: string, imageBlob: Blob) { imageBlob.size )}` ); - const convertedImageBlob = await HEICConverter.convert(imageBlob); + const convertedImageBlob = await heicConversionService.convert( + imageBlob + ); + addLogLine(`${fileName} successfully converted`); return convertedImageBlob; } else { @@ -414,6 +430,19 @@ export async function changeFileName(file: EnteFile, editedName: string) { return file; } +export async function changeCaption(file: EnteFile, caption: string) { + const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = { + caption, + }; + + file.pubMagicMetadata = await updateMagicMetadataProps( + file.pubMagicMetadata ?? NEW_FILE_MAGIC_METADATA, + file.key, + updatedPublicMagicMetadataProps + ); + return file; +} + export function isSharedFile(user: User, file: EnteFile) { if (!user?.id || !file?.ownerID) { return false; @@ -466,14 +495,9 @@ export function getUniqueFiles(files: EnteFile[]) { } }); } -export function getNonTrashedUniqueUserFiles(files: EnteFile[]) { - const user: User = getData(LS_KEYS.USER) ?? {}; - return getUniqueFiles( - files.filter( - (file) => - (typeof file.isTrashed === 'undefined' || !file.isTrashed) && - (!user.id || file.ownerID === user.id) - ) +export function getNonTrashedFiles(files: EnteFile[]) { + return files.filter( + (file) => typeof file.isTrashed === 'undefined' || !file.isTrashed ); } @@ -520,3 +544,46 @@ export const getUserOwnedNonTrashedFiles = (files: EnteFile[]) => { } return files.filter((file) => file.isTrashed || file.ownerID === user.id); }; + +// doesn't work on firefox +export const copyFileToClipboard = async (fileUrl: string) => { + const canvas = document.createElement('canvas'); + const canvasCTX = canvas.getContext('2d'); + const image = new Image(); + + const blobPromise = new Promise((resolve, reject) => { + let timeout: NodeJS.Timeout = null; + try { + image.setAttribute('src', fileUrl); + image.onload = () => { + canvas.width = image.width; + canvas.height = image.height; + canvasCTX.drawImage(image, 0, 0, image.width, image.height); + canvas.toBlob( + (blob) => { + resolve(blob); + }, + 'image/png', + 1 + ); + + clearTimeout(timeout); + }; + } catch (e) { + void logError(e, 'failed to copy to clipboard'); + reject(e); + } finally { + clearTimeout(timeout); + } + timeout = setTimeout( + () => reject(Error(CustomError.WAIT_TIME_EXCEEDED)), + WAIT_TIME_IMAGE_CONVERSION + ); + }); + + const { ClipboardItem } = window; + + await navigator.clipboard + .write([new ClipboardItem({ 'image/png': blobPromise })]) + .catch((e) => logError(e, 'failed to copy to clipboard')); +}; diff --git a/src/utils/file/livePhoto.ts b/src/utils/file/livePhoto.ts new file mode 100644 index 000000000..da0fb4f32 --- /dev/null +++ b/src/utils/file/livePhoto.ts @@ -0,0 +1,41 @@ +import { FILE_TYPE } from 'constants/file'; +import { getFileExtension } from 'utils/file'; + +const IMAGE_EXTENSIONS = [ + 'heic', + 'heif', + 'jpeg', + 'jpg', + 'png', + 'gif', + 'bmp', + 'tiff', + 'webp', +]; + +const VIDEO_EXTENSIONS = [ + 'mov', + 'mp4', + 'm4v', + 'avi', + 'wmv', + 'flv', + 'mkv', + 'webm', + '3gp', + '3g2', + 'avi', + 'ogv', + 'mpg', +]; + +export function getFileTypeFromExtensionForLivePhotoClustering( + filename: string +) { + const extension = getFileExtension(filename); + if (IMAGE_EXTENSIONS.includes(extension)) { + return FILE_TYPE.IMAGE; + } else if (VIDEO_EXTENSIONS.includes(extension)) { + return FILE_TYPE.VIDEO; + } +} diff --git a/src/utils/heicConverter/index.ts b/src/utils/heicConverter/index.ts deleted file mode 100644 index d3a347105..000000000 --- a/src/utils/heicConverter/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ComlinkWorker } from 'utils/comlink'; -import { runningInBrowser } from 'utils/common'; -import * as Comlink from 'comlink'; - -const getDedicatedConvertWorker = (): ComlinkWorker => { - if (runningInBrowser()) { - const worker = new Worker( - new URL('worker/convert.worker.js', import.meta.url), - { name: 'ente-convert-worker' } - ); - const comlink = Comlink.wrap(worker); - return { comlink, worker }; - } -}; - -export const createNewConvertWorker = async () => { - const comlinkWrapperWorker = getDedicatedConvertWorker(); - if (comlinkWrapperWorker) { - return { - comlink: await new comlinkWrapperWorker.comlink(), - worker: comlinkWrapperWorker.worker, - }; - } -}; diff --git a/src/utils/logging/index.ts b/src/utils/logging/index.ts index 9a88b7676..1b08de54c 100644 --- a/src/utils/logging/index.ts +++ b/src/utils/logging/index.ts @@ -1,7 +1,24 @@ import { ElectronFile } from 'types/upload'; import { convertBytesToHumanReadable } from 'utils/file/size'; -import { formatDateTime } from 'utils/time'; -import { saveLogLine, getLogs } from 'utils/storage'; +import { formatDateTimeShort } from 'utils/time/format'; +import { isDEVSentryENV } from 'constants/sentry'; +import isElectron from 'is-electron'; +import ElectronService from 'services/electron/common'; +import { logError } from 'utils/sentry'; +import { + getData, + LS_KEYS, + removeData, + setData, +} from 'utils/storage/localStorage'; + +export const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB +export const MAX_LOG_LINES = 1000; + +export interface Log { + timestamp: number; + logLine: string; +} // commented out need fixing export function pipeConsoleLogsToDebugLogs() { @@ -14,27 +31,91 @@ export function pipeConsoleLogsToDebugLogs() { } export function addLogLine(log: string) { - if (!process.env.NEXT_PUBLIC_SENTRY_ENV) { - console.log(log); + try { + if (isDEVSentryENV()) { + console.log(log); + } + if (isElectron()) { + ElectronService.logToDisk(log); + } else { + saveLogLine({ + timestamp: Date.now(), + logLine: log, + }); + } + } catch (e) { + if (e.name === 'QuotaExceededError') { + deleteLogs(); + addLogLine('logs cleared'); + } + logError(e, 'failed to addLogLine', undefined, true); + // ignore } - saveLogLine({ - timestamp: Date.now(), - logLine: log, - }); } export const addLocalLog = (getLog: () => string) => { - if (!process.env.NEXT_PUBLIC_SENTRY_ENV) { + if (isDEVSentryENV()) { console.log(getLog()); } }; export function getDebugLogs() { - return getLogs().map( - (log) => `[${formatDateTime(log.timestamp)}] ${log.logLine}` - ); + return combineLogLines(getLogs()); } export function getFileNameSize(file: File | ElectronFile) { return `${file.name}_${convertBytesToHumanReadable(file.size)}`; } + +export const clearLogsIfLocalStorageLimitExceeded = () => { + try { + const logs = getDebugLogs(); + const logSize = getStringSize(logs); + if (logSize > MAX_LOG_SIZE) { + deleteLogs(); + addLogLine('Logs cleared due to size limit exceeded'); + } else { + try { + addLogLine(`app started`); + } catch (e) { + deleteLogs(); + } + } + addLogLine(`logs size: ${convertBytesToHumanReadable(logSize)}`); + } catch (e) { + logError(e, 'failed to clearLogsIfLocalStorageLimitExceeded'); + } +}; + +function saveLogLine(log: Log) { + const logs = getLogs(); + if (logs.length > MAX_LOG_LINES) { + logs.slice(logs.length - MAX_LOG_LINES); + } + logs.push(log); + setLogs(logs); +} + +function getLogs(): Log[] { + return getData(LS_KEYS.LOGS)?.logs ?? []; +} + +function setLogs(logs: Log[]) { + setData(LS_KEYS.LOGS, { logs }); +} + +function deleteLogs() { + removeData(LS_KEYS.LOGS); +} + +function getStringSize(str: string) { + return new Blob([str]).size; +} + +function formatLog(log: Log) { + return `[${formatDateTimeShort(log.timestamp)}] ${log.logLine}`; +} + +function combineLogLines(logs: Log[]) { + return logs.map(formatLog).join('\n'); +} diff --git a/src/utils/magicMetadata/index.ts b/src/utils/magicMetadata/index.ts index 2a1171ff3..e8c9d0f52 100644 --- a/src/utils/magicMetadata/index.ts +++ b/src/utils/magicMetadata/index.ts @@ -1,7 +1,11 @@ import { Collection } from 'types/collection'; -import { EnteFile, FileMagicMetadataProps } from 'types/file'; -import { MagicMetadataCore, VISIBILITY_STATE } from 'types/magicMetadata'; -import CryptoWorker from 'utils/crypto'; +import { EnteFile } from 'types/file'; +import { + FileMagicMetadataProps, + MagicMetadataCore, + VISIBILITY_STATE, +} from 'types/magicMetadata'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; export function IsArchived(item: Collection | EnteFile) { if ( @@ -21,13 +25,13 @@ export async function updateMagicMetadataProps( decryptionKey: string, magicMetadataUpdates: Record ) { - const worker = await new CryptoWorker(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); if (!originalMagicMetadata) { throw Error('invalid originalMagicMetadata '); } if (typeof originalMagicMetadata.data === 'string') { - originalMagicMetadata.data = (await worker.decryptMetadata( + originalMagicMetadata.data = (await cryptoWorker.decryptMetadata( originalMagicMetadata.data, originalMagicMetadata.header, decryptionKey diff --git a/src/utils/publicCollectionGallery/index.ts b/src/utils/publicCollectionGallery/index.ts index 6d06413d8..4cb09514c 100644 --- a/src/utils/publicCollectionGallery/index.ts +++ b/src/utils/publicCollectionGallery/index.ts @@ -1,13 +1,14 @@ import { createContext } from 'react'; import { PublicCollectionGalleryContextType } from 'types/publicCollection'; -export const defaultPublicCollectionGalleryContext: PublicCollectionGalleryContextType = +const defaultPublicCollectionGalleryContext: PublicCollectionGalleryContextType = { token: null, passwordToken: null, accessedThroughSharedURL: false, openReportForm: () => null, photoListHeader: null, + photoListFooter: null, }; export const PublicCollectionGalleryContext = diff --git a/src/utils/sentry/index.ts b/src/utils/sentry/index.ts index 35a4d873b..116d35909 100644 --- a/src/utils/sentry/index.ts +++ b/src/utils/sentry/index.ts @@ -1,27 +1,31 @@ import * as Sentry from '@sentry/nextjs'; +import { isDEVSentryENV } from 'constants/sentry'; import { addLogLine } from 'utils/logging'; import { getSentryUserID } from 'utils/user'; -export const logError = ( +export const logError = async ( error: any, msg: string, - info?: Record + info?: Record, + skipAddLogLine = false ) => { if (isErrorUnnecessaryForSentry(error)) { return; } const err = errorWithContext(error, msg); - addLogLine( - `error: ${error?.name} ${error?.message} ${ - error?.stack - } msg: ${msg} info: ${JSON.stringify(info)}` - ); - if (!process.env.NEXT_PUBLIC_SENTRY_ENV) { + if (!skipAddLogLine) { + addLogLine( + `error: ${error?.name} ${error?.message} ${ + error?.stack + } msg: ${msg} info: ${JSON.stringify(info)}` + ); + } + if (isDEVSentryENV()) { console.log(error, { msg, info }); } Sentry.captureException(err, { level: Sentry.Severity.Info, - user: { id: getSentryUserID() }, + user: { id: await getSentryUserID() }, contexts: { ...(info && { info: info, diff --git a/src/utils/storage/index.ts b/src/utils/storage/index.ts index 5971fd461..0b35736cd 100644 --- a/src/utils/storage/index.ts +++ b/src/utils/storage/index.ts @@ -1,12 +1,5 @@ import { getData, LS_KEYS, setData } from './localStorage'; -export interface Log { - timestamp: number; - logLine: string; -} - -const MAX_LOG_LINES = 10000; - export const isFirstLogin = () => getData(LS_KEYS.IS_FIRST_LOGIN)?.status ?? false; @@ -28,13 +21,3 @@ export function getLivePhotoInfoShownCount() { export function setLivePhotoInfoShownCount(count) { setData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT, { count }); } - -export function saveLogLine(log: Log) { - setData(LS_KEYS.LOGS, { - logs: [...getLogs(), log].slice(-1 * MAX_LOG_LINES), - }); -} - -export function getLogs(): Log[] { - return getData(LS_KEYS.LOGS)?.logs ?? []; -} diff --git a/src/utils/storage/localStorage.ts b/src/utils/storage/localStorage.ts index 7674e4822..04443b1db 100644 --- a/src/utils/storage/localStorage.ts +++ b/src/utils/storage/localStorage.ts @@ -40,7 +40,8 @@ export const getData = (key: LS_KEYS) => { if ( typeof localStorage === 'undefined' || typeof key === 'undefined' || - typeof localStorage.getItem(key) === 'undefined' + typeof localStorage.getItem(key) === 'undefined' || + localStorage.getItem(key) === 'undefined' ) { return null; } diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index bbd381e10..5b9149e9f 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -46,7 +46,9 @@ const englishConstants = { NEW_USER: 'New to ente', EXISTING_USER: 'Existing user', NAME: 'Name', - ENTER_NAME: 'Your name', + ENTER_NAME: 'Enter name', + PUBLIC_UPLOADER_NAME_MESSAGE: + 'Add a name so that your friends know who to thank for these great photos!', EMAIL: 'Email', ENTER_EMAIL: 'Enter email address', DATA_DISCLAIMER: "We'll never share your data with anyone else.", @@ -72,8 +74,8 @@ const englishConstants = { UNKNOWN_ERROR: 'Something went wrong, please try again', INVALID_CODE: 'Invalid verification code', EXPIRED_CODE: 'Your verification code has expired', - SENDING: 'sending...', - SENT: 'sent!', + SENDING: 'Sending...', + SENT: 'Sent!', PASSWORD: 'Password', LINK_PASSWORD: 'Enter password to unlock the album', ENTER_PASSPHRASE: 'Enter your password', @@ -100,15 +102,21 @@ const englishConstants = { SELECT_COLLECTION: 'Select an album to upload to', CREATE_COLLECTION: 'New album', ENTER_ALBUM_NAME: 'Album name', + CLOSE_OPTION: 'Close (Esc)', + ENTER_FILE_NAME: 'File name', CLOSE: 'Close', NO: 'No', NOTHING_HERE: 'Nothing to see here yet 👀', UPLOAD: 'Upload', + ADD_MORE_PHOTOS: 'Add more photos', + ADD_PHOTOS: 'Add photos', + SELECT_PHOTOS: 'Select photos', FILE_UPLOAD: 'File Upload', UPLOAD_STAGE_MESSAGE: { 0: 'Preparing to upload', 1: 'Reading google metadata files', - 2: 'Reading file metadata', + 2: (fileCounter) => + `${fileCounter.finished} / ${fileCounter.total} files metadata extracted`, 3: (fileCounter) => `${fileCounter.finished} / ${fileCounter.total} files backed up`, 4: 'Cancelling remaining uploads', @@ -132,10 +140,12 @@ const englishConstants = { ALBUM_NAME: 'Album name', CREATE: 'Create', DOWNLOAD: 'Download', - TOGGLE_FULLSCREEN: 'Toggle fullscreen', + DOWNLOAD_OPTION: 'Download (D)', + COPY_OPTION: 'Copy as PNG (Ctrl/Cmd - C)', + TOGGLE_FULLSCREEN: 'Toggle fullscreen (F)', ZOOM_IN_OUT: 'Zoom in/out', - PREVIOUS: 'Previous (arrow left)', - NEXT: 'Next (arrow right)', + PREVIOUS: 'Previous (←)', + NEXT: 'Next (→)', NO_INTERNET_CONNECTION: 'Please check your internet connection and try again', TITLE: 'ente.io | encrypted photo storage', @@ -147,14 +157,18 @@ const englishConstants = { UPLOAD_FIRST_PHOTO: 'Preserve', UPLOAD_DROPZONE_MESSAGE: 'Drop to backup your files', WATCH_FOLDER_DROPZONE_MESSAGE: 'Drop to add watched folder', - CONFIRM_DELETE: 'Confirm deletion', - DELETE_MESSAGE: `The selected files will be permanently deleted and can't be restored `, TRASH_FILES_TITLE: 'Delete files?', + TRASH_FILE_TITLE: 'Delete file?', DELETE_FILES_TITLE: 'Delete immediately?', DELETE_FILES_MESSAGE: 'Selected files will be permanently deleted from your ente account.', DELETE_FILE: 'Delete files', DELETE: 'Delete', + DELETE_OPTION: 'Delete (DEL)', + FAVORITE: 'Favorite', + FAVORITE_OPTION: 'Favorite (L)', + UNFAVORITE_OPTION: 'Unfavorite (L)', + UNFAVORITE: 'Unfavorite', MULTI_FOLDER_UPLOAD: 'Multiple folders detected', UPLOAD_STRATEGY_CHOICE: 'Would you like to upload them into', UPLOAD_STRATEGY_SINGLE_COLLECTION: 'A single album', @@ -229,7 +243,7 @@ const englishConstants = { LOGOUT_MESSAGE: 'Are you sure you want to logout?', CHANGE: 'Change', CHANGE_EMAIL: 'Change email', - OK: 'Ok', + OK: 'OK', SUCCESS: 'Success', ERROR: 'Error', MESSAGE: 'Message', @@ -362,6 +376,7 @@ const englishConstants = { ), RENAME: 'Rename', + RENAME_FILE: 'Rename file', RENAME_COLLECTION: 'Rename album', DELETE_COLLECTION_TITLE: 'Delete album?', DELETE_COLLECTION: 'Delete album', @@ -385,12 +400,11 @@ const englishConstants = {

All files will be queued for download sequentially

), - ARCHIVED_ALBUM: 'Archived album', DOWNLOAD_COLLECTION_FAILED: 'Album downloading failed, please try again', CREATE_ALBUM_FAILED: 'Failed to create album , please try again', SEARCH_RESULTS: 'Search results', - SEARCH_HINT: () => Search for location, dates, albums ..., + SEARCH_HINT: () => Search for albums, dates ..., SEARCH_TYPE: (type: SuggestionType) => { switch (type) { case SuggestionType.COLLECTION: @@ -458,14 +472,20 @@ const englishConstants = { TEXT: 'text', METADATA: 'Metadata', - INFO: 'Info', - FILE_ID: 'File id', + INFO: 'Info ', + INFO_OPTION: 'Info (I)', + FILE_ID: 'File ID', FILE_NAME: 'File name', + CAPTION: 'Description', + CAPTION_PLACEHOLDER: 'Add a description', CREATION_TIME: 'Creation time', UPDATED_ON: 'Updated on', LOCATION: 'Location', - SHOW_MAP: 'show on map', - EXIF: 'Exif', + SHOW_ON_MAP: 'View on OpenStreetMap', + DETAILS: 'Details', + VIEW_EXIF: 'View all EXIF data', + NO_EXIF: 'No EXIF data', + EXIF: 'EXIF', DEVICE: 'Device', IMAGE_SIZE: 'Image size', FLASH: 'Flash', @@ -534,9 +554,9 @@ const englishConstants = { RETRY: 'Retry', SEND_OTT: 'Send OTP', EMAIl_ALREADY_OWNED: 'Email already taken', - EMAIL_UDPATE_SUCCESSFUL: 'Your email has been udpated successfully', + EMAIL_UDPATE_SUCCESSFUL: 'Your email has been updated successfully', UPLOAD_FAILED: 'Upload failed', - ETAGS_BLOCKED: (url: string) => ( + ETAGS_BLOCKED: (link: string) => ( <> We were unable to upload the following files because of your @@ -545,20 +565,31 @@ const englishConstants = { Please disable any addons that might be preventing ente from using eTags to upload large files, or use our{' '} - + desktop app - {' '} + {' '} for a more reliable import experience. ), + SKIPPED_VIDEOS_INFO: (link: string) => ( + <> + + Presently we do not support adding videos via public links.{' '} + + + To share videos, please{' '} + + signup + {' '} + for ente and share with the intended recipients using their + email. + + + ), LIVE_PHOTOS_DETECTED: - 'The photo and video files from your Live Photos have been merged into a single ELP file', + 'The photo and video files from your Live Photos have been merged into a single file', RETRY_FAILED: 'Retry failed uploads', FAILED_UPLOADS: 'Failed uploads ', @@ -570,6 +601,8 @@ const englishConstants = { 'Skipped these as there are files with matching names in the same album', UNSUPPORTED_INFO: 'ente does not support these file formats yet', BLOCKED_UPLOADS: 'Blocked uploads', + SKIPPED_VIDEOS: 'Skipped videos', + INPROGRESS_METADATA_EXTRACTION: 'In progress', INPROGRESS_UPLOADS: 'Uploads in progress', TOO_LARGE_UPLOADS: 'Large files', LARGER_THAN_AVAILABLE_STORAGE_UPLOADS: 'Insufficient storage', @@ -580,11 +613,11 @@ const englishConstants = { THUMBNAIL_GENERATION_FAILED_INFO: 'These files were uploaded, but unfortunately we could not generate the thumbnails for them.', UPLOAD_TO_COLLECTION: 'Upload to album', - ARCHIVE: 'Hide', - ARCHIVE_SECTION_NAME: 'Hidden', + ARCHIVE: 'Archive', + ARCHIVE_SECTION_NAME: 'Archive', ALL_SECTION_NAME: 'All', MOVE_TO_COLLECTION: 'Move to album', - UNARCHIVE: 'Unhide', + UNARCHIVE: 'Unarchive', MOVE: 'Move', ADD: 'Add', SORT: 'Sort', @@ -594,6 +627,8 @@ const englishConstants = { MOVE_TO_TRASH: 'Move to trash', TRASH_FILES_MESSAGE: 'Selected files will be removed from all albums and moved to trash.', + TRASH_FILE_MESSAGE: + 'The file will be removed from all albums and moved to trash.', DELETE_PERMANENTLY: 'Delete permanently', RESTORE: 'Restore', CONFIRM_RESTORE: 'Confirm restoration', @@ -603,7 +638,12 @@ const englishConstants = { EMPTY_TRASH_TITLE: 'Empty trash?', EMPTY_TRASH_MESSAGE: 'These files will be permanently deleted from your ente account.', - + LEAVE_SHARED_ALBUM: 'Yes, leave', + LEAVE_ALBUM: 'Leave album', + LEAVE_SHARED_ALBUM_TITLE: 'Leave shared album?', + LEAVE_SHARED_ALBUM_FAILED: 'failed to leave the album, please try again', + LEAVE_SHARED_ALBUM_MESSAGE: + 'You will leave the album, and it will stop being visible to you.', CONFIRM_REMOVE_MESSAGE: () => ( <>

Are you sure you want to remove these files from the album?

@@ -649,6 +689,7 @@ const englishConstants = { <>File time updation failed for some files, please retry ), FILE_NAME_CHARACTER_LIMIT: '100 characters max', + CAPTION_CHARACTER_LIMIT: '5000 characters max', DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal', DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized', @@ -667,7 +708,8 @@ const englishConstants = { ALBUM_URL: 'Album url', PUBLIC_SHARING: 'Public link', NOT_FOUND: '404 - not found', - LINK_EXPIRED: 'This link has either expired or been disabled!', + LINK_EXPIRED: 'Link expired', + LINK_EXPIRED_MESSAGE: 'This link has either expired or been disabled!', MANAGE_LINK: 'Manage link', LINK_TOO_MANY_REQUESTS: 'This album is too popular for us to handle!', DISABLE_PUBLIC_SHARING: "'Disable public sharing", @@ -675,6 +717,7 @@ const englishConstants = { 'Are you sure you want to disable public sharing?', FILE_DOWNLOAD: 'Allow downloads', LINK_PASSWORD_LOCK: 'Password lock', + PUBLIC_COLLECT: 'Allow adding photos', LINK_DEVICE_LIMIT: 'Device limit', LINK_EXPIRY: 'Link expiry', LINK_EXPIRY_NEVER: 'Never', @@ -687,7 +730,7 @@ const englishConstants = {

{' '}

Viewers can still take screenshots or save a copy of your photos - using external tools'{' '} + using external tools{' '}

), @@ -719,7 +762,7 @@ const englishConstants = { TERM_1: 'I hereby state that I have a good faith belief that the sharing of copyrighted material at the location above is not authorized by the copyright owner, its agent, or the law (e.g., as a fair use). ', TERM_2: 'I hereby state that the information in this Notice is accurate and, under penalty of perjury, that I am the owner, or authorized to act on behalf of, the owner, of the copyright or of an exclusive right under the copyright that is allegedly infringed. ', TERM_3: 'I acknowledge that any person who knowingly materially misrepresents that material or activity is infringing may be subject to liability for damages. ', - PRESERVED_BY: 'preserved by', + SHARED_USING: 'Shared using ', ENTE_IO: 'ente.io', LIVE: 'LIVE', DISABLE_PASSWORD: 'Disable password lock', @@ -847,6 +890,31 @@ const englishConstants = { UPLOADED_TO_SINGLE_COLLECTION: 'Uploaded to single collection', UPLOADED_TO_SEPARATE_COLLECTIONS: 'Uploaded to separate collections', NEVERMIND: 'Nevermind', + UPDATE_AVAILABLE: 'Update available', + UPDATE_INSTALLABLE_MESSAGE: + 'A new version of ente is ready to be installed.', + INSTALL_NOW: `Install now`, + INSTALL_ON_NEXT_LAUNCH: 'Install on next launch', + UPDATE_AVAILABLE_MESSAGE: + 'A new version of ente has been released, but it cannot be automatically downloaded and installed.', + DOWNLOAD_AND_INSTALL: 'Download and install', + IGNORE_THIS_VERSION: 'Ignore this version', + TODAY: 'Today', + YESTERDAY: 'Yesterday', + AT: 'at', + NAME_PLACEHOLDER: 'Name...', + ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED: + 'Cannot create albums from file/folder mix', + ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE: () => ( + <> +

You have dragged and dropped a mixture of files and folders.

+

+ Please provide either only files, or only folders when selecting + option to create separate albums +

+ + ), + ADD_X_PHOTOS: (x: number) => `Add ${x} ${x > 1 ? 'photos' : 'photo'}`, }; export default englishConstants; diff --git a/src/utils/temp/index.ts b/src/utils/temp/index.ts new file mode 100644 index 000000000..589c66d55 --- /dev/null +++ b/src/utils/temp/index.ts @@ -0,0 +1,14 @@ +const CHARACTERS = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + +export function generateTempName(length: number, suffix: string) { + let tempName = ''; + + const charactersLength = CHARACTERS.length; + for (let i = 0; i < length; i++) { + tempName += CHARACTERS.charAt( + Math.floor(Math.random() * charactersLength) + ); + } + return `${tempName}-${suffix}`; +} diff --git a/src/utils/time/format.ts b/src/utils/time/format.ts new file mode 100644 index 000000000..aa0502525 --- /dev/null +++ b/src/utils/time/format.ts @@ -0,0 +1,85 @@ +export function formatDateFull(date: number | Date) { + const dateTimeFormat1 = new Intl.DateTimeFormat('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + }); + + const dateTimeFormat2 = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + }); + + return [dateTimeFormat1, dateTimeFormat2] + .map((f) => f.format(date)) + .join(' '); +} + +export function formatDate(date: number | Date) { + const dateTimeFormat1 = new Intl.DateTimeFormat('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + }); + const withinYear = + new Date().getFullYear() === new Date(date).getFullYear(); + const dateTimeFormat2 = !withinYear + ? new Intl.DateTimeFormat('en-US', { + year: 'numeric', + }) + : null; + return [dateTimeFormat1, dateTimeFormat2] + .filter((f) => !!f) + .map((f) => f.format(date)) + .join(' '); +} + +export function formatDateTimeShort(date: number | Date) { + const dateTimeFormat = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + + return dateTimeFormat.format(date); +} + +export function formatTime(date: number | Date) { + const timeFormat = new Intl.DateTimeFormat('en-IN', { + timeStyle: 'short', + }); + return timeFormat.format(date).toUpperCase(); +} + +export function formatDateTimeFull(dateTime: number | Date): string { + return [formatDateFull(dateTime), 'at', formatTime(dateTime)].join(' '); +} + +export function formatDateTime(dateTime: number | Date): string { + return [formatDate(dateTime), 'at', formatTime(dateTime)].join(' '); +} + +export function formatDateRelative(date: number) { + const units = { + year: 24 * 60 * 60 * 1000 * 365, + month: (24 * 60 * 60 * 1000 * 365) / 12, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + second: 1000, + }; + const relativeDateFormat = new Intl.RelativeTimeFormat('en-IN', { + localeMatcher: 'best fit', + numeric: 'always', + style: 'long', + }); + const elapsed = date - Date.now(); // "Math.abs" accounts for both "past" & "future" scenarios + + for (const u in units) + if (Math.abs(elapsed) > units[u] || u === 'second') + return relativeDateFormat.format( + Math.round(elapsed / units[u]), + u as Intl.RelativeTimeFormatUnit + ); +} diff --git a/src/utils/time/index.ts b/src/utils/time/index.ts index 56fa78d78..58bf1e39c 100644 --- a/src/utils/time/index.ts +++ b/src/utils/time/index.ts @@ -14,25 +14,6 @@ interface DateComponent { second: T; } -export function dateStringWithMMH(unixTimeInMicroSeconds: number): string { - return new Date(unixTimeInMicroSeconds / 1000).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -} - -export function formatDateShort(date: number | Date) { - const dateTimeFormat = new Intl.DateTimeFormat('en-IN', { - year: '2-digit', - month: 'short', - day: 'numeric', - }); - return dateTimeFormat.format(date); -} - export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number { let currentDate = new Date(); if (delta?.hours) { @@ -55,7 +36,8 @@ export function getUnixTimeInMicroSeconds(dateTime: Date) { return null; } const unixTime = dateTime.getTime() * 1000; - if (unixTime <= 0) { + //ignoring dateTimeString = "0000:00:00 00:00:00" + if (unixTime === Date.UTC(0, 0, 0, 0, 0, 0, 0) || unixTime === 0) { return null; } else { return unixTime; @@ -163,39 +145,3 @@ function getDateFromComponents(dateComponent: DateComponent) { ? new Date(year, month, day, hour, minute, second) : new Date(year, month, day); } - -export function formatDateTime(date: number | Date) { - const dateTimeFormat = new Intl.DateTimeFormat('en-IN', { - weekday: 'short', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - const timeFormat = new Intl.DateTimeFormat('en-IN', { - timeStyle: 'short', - }); - return `${dateTimeFormat.format(date)} ${timeFormat.format(date)}`; -} -export function formatDateRelative(date: number) { - const units = { - year: 24 * 60 * 60 * 1000 * 365, - month: (24 * 60 * 60 * 1000 * 365) / 12, - day: 24 * 60 * 60 * 1000, - hour: 60 * 60 * 1000, - minute: 60 * 1000, - second: 1000, - }; - const relativeDateFormat = new Intl.RelativeTimeFormat('en-IN', { - localeMatcher: 'best fit', - numeric: 'always', - style: 'long', - }); - const elapsed = date - Date.now(); // "Math.abs" accounts for both "past" & "future" scenarios - - for (const u in units) - if (Math.abs(elapsed) > units[u] || u === 'second') - return relativeDateFormat.format( - Math.round(elapsed / units[u]), - u as Intl.RelativeTimeFormatUnit - ); -} diff --git a/src/utils/ui/index.tsx b/src/utils/ui/index.tsx index 63668ad83..f1ffabac2 100644 --- a/src/utils/ui/index.tsx +++ b/src/utils/ui/index.tsx @@ -1,7 +1,11 @@ +import React from 'react'; +import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; import { DialogBoxAttributes } from 'types/dialogBox'; import { downloadApp } from 'utils/common'; import constants from 'utils/strings/constants'; - +import ElectronUpdateService from 'services/electron/update'; +import { AppUpdateInfo } from 'types/electron'; +import InfoOutlined from '@mui/icons-material/InfoRounded'; export const getDownloadAppMessage = (): DialogBoxAttributes => { return { title: constants.DOWNLOAD_APP, @@ -30,3 +34,55 @@ export const getTrashFilesMessage = ( }, close: { text: constants.CANCEL }, }); + +export const getTrashFileMessage = (deleteFileHelper): DialogBoxAttributes => ({ + title: constants.TRASH_FILE_TITLE, + content: constants.TRASH_FILE_MESSAGE, + proceed: { + action: deleteFileHelper, + text: constants.MOVE_TO_TRASH, + variant: 'danger', + }, + close: { text: constants.CANCEL }, +}); + +export const getUpdateReadyToInstallMessage = (): DialogBoxAttributes => ({ + icon: , + title: constants.UPDATE_AVAILABLE, + content: constants.UPDATE_INSTALLABLE_MESSAGE, + close: { + text: constants.INSTALL_ON_NEXT_LAUNCH, + variant: 'secondary', + }, + proceed: { + action: () => ElectronUpdateService.updateAndRestart(), + text: constants.INSTALL_NOW, + variant: 'accent', + }, +}); + +export const getUpdateAvailableForDownloadMessage = ( + updateInfo: AppUpdateInfo +): DialogBoxAttributes => ({ + icon: , + title: constants.UPDATE_AVAILABLE, + content: constants.UPDATE_AVAILABLE_MESSAGE, + close: { + text: constants.IGNORE_THIS_VERSION, + variant: 'secondary', + action: () => ElectronUpdateService.skipAppVersion(updateInfo.version), + }, + proceed: { + action: downloadApp, + text: constants.DOWNLOAD_AND_INSTALL, + variant: 'accent', + }, +}); + +export const getRootLevelFileWithFolderNotAllowMessage = + (): DialogBoxAttributes => ({ + icon: , + title: constants.ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED, + content: constants.ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE(), + close: {}, + }); diff --git a/src/utils/upload/index.ts b/src/utils/upload/index.ts index 73ad87922..cede5776f 100644 --- a/src/utils/upload/index.ts +++ b/src/utils/upload/index.ts @@ -128,6 +128,7 @@ export function getImportSuggestion( let i = 0; const firstFileFolder = firstPath.substring(0, firstPath.lastIndexOf('/')); const lastFileFolder = lastPath.substring(0, lastPath.lastIndexOf('/')); + while (i < L && firstPath.charAt(i) === lastPath.charAt(i)) i++; let commonPathPrefix = firstPath.substring(0, i); @@ -145,6 +146,7 @@ export function getImportSuggestion( return { rootFolderName: commonPathPrefix || null, hasNestedFolders: firstFileFolder !== lastFileFolder, + hasRootLevelFileWithFolder: firstFileFolder === '', }; } diff --git a/src/utils/user/index.ts b/src/utils/user/index.ts index cfafd92a7..f1914cbf4 100644 --- a/src/utils/user/index.ts +++ b/src/utils/user/index.ts @@ -1,5 +1,7 @@ -import { UserDetails } from 'types/user'; +import isElectron from 'is-electron'; +import { User, UserDetails } from 'types/user'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; +import ElectronService from 'services/electron/common'; export function makeID(length) { let result = ''; @@ -14,15 +16,22 @@ export function makeID(length) { return result; } -export function getSentryUserID() { - let anonymizeUserID = getData(LS_KEYS.AnonymizedUserID)?.id; - if (!anonymizeUserID) { - anonymizeUserID = makeID(6); - setData(LS_KEYS.AnonymizedUserID, { id: anonymizeUserID }); +export async function getSentryUserID() { + if (isElectron()) { + return await ElectronService.getSentryUserID(); + } else { + let anonymizeUserID = getData(LS_KEYS.AnonymizedUserID)?.id; + if (!anonymizeUserID) { + anonymizeUserID = makeID(6); + setData(LS_KEYS.AnonymizedUserID, { id: anonymizeUserID }); + } + return anonymizeUserID; } - return anonymizeUserID; } export function getLocalUserDetails(): UserDetails { return getData(LS_KEYS.USER_DETAILS)?.value; } + +export const isInternalUser = () => + (getData(LS_KEYS.USER) as User)?.email.endsWith('@ente.io'); diff --git a/src/worker/convert.worker.js b/src/worker/convert.worker.js deleted file mode 100644 index ee1a7ca55..000000000 --- a/src/worker/convert.worker.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as Comlink from 'comlink'; -import { convertHEIC } from 'services/heicConverter/heicConverterClient'; - -export class Convert { - async convertHEIC(fileBlob, format) { - return convertHEIC(fileBlob, format); - } -} - -Comlink.expose(Convert); diff --git a/src/worker/convert.worker.ts b/src/worker/convert.worker.ts new file mode 100644 index 000000000..8dae977a9 --- /dev/null +++ b/src/worker/convert.worker.ts @@ -0,0 +1,10 @@ +import * as Comlink from 'comlink'; +import { convertHEIC } from 'services/wasmHeicConverter/wasmHEICConverterClient'; + +export class DedicatedConvertWorker { + async convertHEIC(fileBlob: Blob, format: string) { + return convertHEIC(fileBlob, format); + } +} + +Comlink.expose(DedicatedConvertWorker, self); diff --git a/src/worker/crypto.worker.js b/src/worker/crypto.worker.ts similarity index 59% rename from src/worker/crypto.worker.js rename to src/worker/crypto.worker.ts index f2bce17a6..d5d193279 100644 --- a/src/worker/crypto.worker.js +++ b/src/worker/crypto.worker.ts @@ -1,8 +1,13 @@ import * as Comlink from 'comlink'; +import { StateAddress } from 'libsodium-wrappers'; import * as libsodium from 'utils/crypto/libsodium'; -export class Crypto { - async decryptMetadata(encryptedMetadata, header, key) { +export class DedicatedCryptoWorker { + async decryptMetadata( + encryptedMetadata: string, + header: string, + key: string + ) { const encodedMetadata = await libsodium.decryptChaChaOneShot( await libsodium.fromB64(encryptedMetadata), await libsodium.fromB64(header), @@ -11,15 +16,19 @@ export class Crypto { return JSON.parse(new TextDecoder().decode(encodedMetadata)); } - async decryptThumbnail(fileData, header, key) { + async decryptThumbnail( + fileData: Uint8Array, + header: Uint8Array, + key: string + ) { return libsodium.decryptChaChaOneShot(fileData, header, key); } - async decryptFile(fileData, header, key) { + async decryptFile(fileData: Uint8Array, header: Uint8Array, key: string) { return libsodium.decryptChaCha(fileData, header, key); } - async encryptMetadata(metadata, key) { + async encryptMetadata(metadata: Object, key: string) { const encodedMetadata = new TextEncoder().encode( JSON.stringify(metadata) ); @@ -36,15 +45,19 @@ export class Crypto { }; } - async encryptThumbnail(fileData, key) { + async encryptThumbnail(fileData: Uint8Array, key: string) { return libsodium.encryptChaChaOneShot(fileData, key); } - async encryptFile(fileData, key) { + async encryptFile(fileData: Uint8Array, key: string) { return libsodium.encryptChaCha(fileData, key); } - async encryptFileChunk(data, pushState, finalChunk) { + async encryptFileChunk( + data: Uint8Array, + pushState: StateAddress, + finalChunk: boolean + ) { return libsodium.encryptFileChunk(data, pushState, finalChunk); } @@ -52,27 +65,27 @@ export class Crypto { return libsodium.initChunkEncryption(); } - async initDecryption(header, key) { + async initDecryption(header: Uint8Array, key: Uint8Array) { return libsodium.initChunkDecryption(header, key); } - async decryptChunk(fileData, pullState) { + async decryptChunk(fileData: Uint8Array, pullState: StateAddress) { return libsodium.decryptChunk(fileData, pullState); } - async encrypt(data, key) { + async encrypt(data: Uint8Array, key: Uint8Array) { return libsodium.encrypt(data, key); } - async decrypt(data, nonce, key) { + async decrypt(data: Uint8Array, nonce: Uint8Array, key: Uint8Array) { return libsodium.decrypt(data, nonce, key); } - async hash(input) { + async hash(input: string) { return libsodium.hash(input); } - async verifyHash(hash, input) { + async verifyHash(hash: string, input: string) { return libsodium.verifyHash(hash, input); } @@ -80,39 +93,44 @@ export class Crypto { return libsodium.initChunkHashing(); } - async hashFileChunk(hashState, chunk) { + async hashFileChunk(hashState: StateAddress, chunk: Uint8Array) { return libsodium.hashFileChunk(hashState, chunk); } - async completeChunkHashing(hashState) { + async completeChunkHashing(hashState: StateAddress) { return libsodium.completeChunkHashing(hashState); } - async deriveKey(passphrase, salt, opsLimit, memLimit) { + async deriveKey( + passphrase: string, + salt: string, + opsLimit: number, + memLimit: number + ) { return libsodium.deriveKey(passphrase, salt, opsLimit, memLimit); } - async deriveSensitiveKey(passphrase, salt) { + async deriveSensitiveKey(passphrase: string, salt: string) { return libsodium.deriveSensitiveKey(passphrase, salt); } - async deriveInteractiveKey(passphrase, salt) { + async deriveInteractiveKey(passphrase: string, salt: string) { return libsodium.deriveInteractiveKey(passphrase, salt); } - async decryptB64(data, nonce, key) { + async decryptB64(data: string, nonce: string, key: string) { return libsodium.decryptB64(data, nonce, key); } - async decryptToUTF8(data, nonce, key) { + async decryptToUTF8(data: string, nonce: string, key: string) { return libsodium.decryptToUTF8(data, nonce, key); } - async encryptToB64(data, key) { + async encryptToB64(data: string, key?: string) { return libsodium.encryptToB64(data, key); } - async encryptUTF8(data, key) { + async encryptUTF8(data: string, key: string) { return libsodium.encryptUTF8(data, key); } @@ -128,37 +146,37 @@ export class Crypto { return libsodium.generateKeyPair(); } - async boxSealOpen(input, publicKey, secretKey) { + async boxSealOpen(input: string, publicKey: string, secretKey: string) { return libsodium.boxSealOpen(input, publicKey, secretKey); } - async boxSeal(input, publicKey) { + async boxSeal(input: string, publicKey: string) { return libsodium.boxSeal(input, publicKey); } - async fromString(string) { + async fromString(string: string) { return libsodium.fromString(string); } - async toB64(data) { + async toB64(data: Uint8Array) { return libsodium.toB64(data); } - async toURLSafeB64(data) { + async toURLSafeB64(data: Uint8Array) { return libsodium.toURLSafeB64(data); } - async fromB64(string) { + async fromB64(string: string) { return libsodium.fromB64(string); } - async toHex(string) { + async toHex(string: string) { return libsodium.toHex(string); } - async fromHex(string) { + async fromHex(string: string) { return libsodium.fromHex(string); } } -Comlink.expose(Crypto); +Comlink.expose(DedicatedCryptoWorker, self); diff --git a/src/worker/ffmpeg.worker.js b/src/worker/ffmpeg.worker.js deleted file mode 100644 index 430781d17..000000000 --- a/src/worker/ffmpeg.worker.js +++ /dev/null @@ -1,21 +0,0 @@ -import * as Comlink from 'comlink'; -import FFmpegClient from 'services/ffmpeg/ffmpegClient'; - -export class FFmpeg { - ffmpegClient; - constructor() { - this.ffmpegClient = new FFmpegClient(); - } - async generateThumbnail(file) { - return this.ffmpegClient.generateThumbnail(file); - } - async extractVideoMetadata(file) { - return this.ffmpegClient.extractVideoMetadata(file); - } - - async convertToMP4(file, inputFileName) { - return this.ffmpegClient.convertToMP4(file, inputFileName); - } -} - -Comlink.expose(FFmpeg); diff --git a/src/worker/ffmpeg.worker.ts b/src/worker/ffmpeg.worker.ts new file mode 100644 index 000000000..6a3ff52c3 --- /dev/null +++ b/src/worker/ffmpeg.worker.ts @@ -0,0 +1,15 @@ +import * as Comlink from 'comlink'; +import { WasmFFmpeg } from 'services/wasm/ffmpeg'; + +export class DedicatedFFmpegWorker { + wasmFFmpeg: WasmFFmpeg; + constructor() { + this.wasmFFmpeg = new WasmFFmpeg(); + } + + run(cmd, inputFile, outputFileName) { + return this.wasmFFmpeg.run(cmd, inputFile, outputFileName); + } +} + +Comlink.expose(DedicatedFFmpegWorker, self); diff --git a/tests/upload.test.ts b/tests/upload.test.ts new file mode 100644 index 000000000..53a0fc6ae --- /dev/null +++ b/tests/upload.test.ts @@ -0,0 +1,256 @@ +import { getLocalFiles } from 'services/fileService'; +import { getLocalCollections } from 'services/collectionService'; +import { getUserDetailsV2 } from 'services/userService'; +import { groupFilesBasedOnCollectionID } from 'utils/file'; +import { FILE_TYPE } from 'constants/file'; + +export async function testUpload() { + if (!process.env.NEXT_PUBLIC_EXPECTED_JSON_PATH) { + throw Error( + 'upload test failed NEXT_PUBLIC_EXPECTED_JSON_PATH missing' + ); + } + const expectedState = await import( + process.env.NEXT_PUBLIC_EXPECTED_JSON_PATH + ); + if (!expectedState) { + throw Error('upload test failed expectedState missing'); + } + + try { + await totalCollectionCountCheck(expectedState); + await collectionWiseFileCount(expectedState); + await thumbnailGenerationFailedFilesCheck(expectedState); + await livePhotoClubbingCheck(expectedState); + await exifDataParsingCheck(expectedState); + await googleMetadataReadingCheck(expectedState); + await totalFileCountCheck(expectedState); + } catch (e) { + console.log(e); + } +} + +async function totalFileCountCheck(expectedState) { + const userDetails = await getUserDetailsV2(); + if (expectedState['total_file_count'] === userDetails.fileCount) { + console.log('file count check passed ✅'); + } else { + throw Error( + `total file count check failed ❌, expected: ${expectedState['total_file_count']}, got: ${userDetails.fileCount}` + ); + } +} + +async function totalCollectionCountCheck(expectedState) { + const collections = await getLocalCollections(); + const files = await getLocalFiles(); + const nonEmptyCollectionIds = new Set( + files.map((file) => file.collectionID) + ); + const nonEmptyCollections = collections.filter((collection) => + nonEmptyCollectionIds.has(collection.id) + ); + if (expectedState['collection_count'] === nonEmptyCollections.length) { + console.log('collection count check passed ✅'); + } else { + throw Error( + `total Collection count check failed ❌ + expected : ${expectedState['collection_count']}, got: ${nonEmptyCollections.length}` + ); + } +} + +async function collectionWiseFileCount(expectedState) { + const files = await getLocalFiles(); + const collections = await getLocalCollections(); + const collectionToFilesMap = groupFilesBasedOnCollectionID(files); + const collectionIDToNameMap = new Map( + collections.map((collection) => [collection.id, collection.name]) + ); + const collectionNameToFileCount = new Map( + [...collectionToFilesMap.entries()].map(([collectionID, files]) => [ + collectionIDToNameMap.get(collectionID), + files.length, + ]) + ); + Object.entries(expectedState['collection_files_count']).forEach( + ([collectionName, fileCount]) => { + if (fileCount !== collectionNameToFileCount.get(collectionName)) { + throw Error( + `collectionWiseFileCount check failed ❌ + for collection ${collectionName} + expected File count : ${fileCount} , got: ${collectionNameToFileCount.get( + collectionName + )}` + ); + } + } + ); + console.log('collection wise file count check passed ✅'); +} + +async function thumbnailGenerationFailedFilesCheck(expectedState) { + const files = await getLocalFiles(); + const filesWithStaticThumbnail = files.filter( + (file) => file.metadata.hasStaticThumbnail + ); + + const fileIDSet = new Set(); + const uniqueFilesWithStaticThumbnail = filesWithStaticThumbnail.filter( + (file) => { + if (fileIDSet.has(file.id)) { + return false; + } else { + fileIDSet.add(file.id); + return true; + } + } + ); + const fileNamesWithStaticThumbnail = uniqueFilesWithStaticThumbnail.map( + (file) => file.metadata.title + ); + + if ( + expectedState['thumbnail_generation_failure']['count'] < + uniqueFilesWithStaticThumbnail.length + ) { + throw Error( + `thumbnailGenerationFailedFiles Count Check failed ❌ + expected: ${expectedState['thumbnail_generation_failure']['count']}, got: ${uniqueFilesWithStaticThumbnail.length}` + ); + } + fileNamesWithStaticThumbnail.forEach((fileName) => { + if ( + !expectedState['thumbnail_generation_failure']['files'].includes( + fileName + ) + ) { + throw Error( + `thumbnailGenerationFailedFiles Check failed ❌ + expected: ${expectedState['thumbnail_generation_failure']['files']}, got: ${fileNamesWithStaticThumbnail}` + ); + } + }); + console.log('thumbnail generation failure check passed ✅'); +} + +async function livePhotoClubbingCheck(expectedState) { + const files = await getLocalFiles(); + const livePhotos = files.filter( + (file) => file.metadata.fileType === FILE_TYPE.LIVE_PHOTO + ); + + const fileIDSet = new Set(); + const uniqueLivePhotos = livePhotos.filter((file) => { + if (fileIDSet.has(file.id)) { + return false; + } else { + fileIDSet.add(file.id); + return true; + } + }); + + const livePhotoFileNames = uniqueLivePhotos.map( + (file) => file.metadata.title + ); + + if (expectedState['live_photo']['count'] !== livePhotoFileNames.length) { + throw Error( + `livePhotoClubbing Check failed ❌ + expected: ${expectedState['live_photo']['count']}, got: ${livePhotoFileNames.length}` + ); + } + expectedState['live_photo']['files'].forEach((fileName) => { + if (!livePhotoFileNames.includes(fileName)) { + throw Error( + `livePhotoClubbing Check failed ❌ + expected: ${expectedState['live_photo']['files']}, got: ${livePhotoFileNames}` + ); + } + }); + console.log('live-photo clubbing check passed ✅'); +} + +async function exifDataParsingCheck(expectedState) { + const files = await getLocalFiles(); + Object.entries(expectedState['exif']).map(([fileName, exifValues]) => { + const matchingFile = files.find( + (file) => file.metadata.title === fileName + ); + if (!matchingFile) { + throw Error(`exifDataParsingCheck failed , ${fileName} missing`); + } + if ( + exifValues['creation_time'] && + exifValues['creation_time'] !== matchingFile.metadata.creationTime + ) { + throw Error(`exifDataParsingCheck failed ❌ , + for ${fileName} + expected: ${exifValues['creation_time']} got: ${matchingFile.metadata.creationTime}`); + } + if ( + exifValues['location'] && + (Math.abs( + exifValues['location']['latitude'] - + matchingFile.metadata.latitude + ) > 1 || + Math.abs( + exifValues['location']['longitude'] - + matchingFile.metadata.longitude + ) > 1) + ) { + throw Error(`exifDataParsingCheck failed ❌ , + for ${fileName} + expected: ${JSON.stringify(exifValues['location'])} + got: [${matchingFile.metadata.latitude},${ + matchingFile.metadata.longitude + }]`); + } + }); + console.log('exif data parsing check passed ✅'); +} + +async function googleMetadataReadingCheck(expectedState) { + const files = await getLocalFiles(); + Object.entries(expectedState['google_import']).map( + ([fileName, metadata]) => { + const matchingFile = files.find( + (file) => file.metadata.title === fileName + ); + if (!matchingFile) { + throw Error( + `exifDataParsingCheck failed , ${fileName} missing` + ); + } + if ( + metadata['creation_time'] && + metadata['creation_time'] !== matchingFile.metadata.creationTime + ) { + throw Error(`googleMetadataJSON reading check failed ❌ , + for ${fileName} + expected: ${metadata['creation_time']} got: ${matchingFile.metadata.creationTime}`); + } + if ( + metadata['location'] && + (Math.abs( + metadata['location']['latitude'] - + matchingFile.metadata.latitude + ) > 1 || + Math.abs( + metadata['location']['longitude'] - + matchingFile.metadata.longitude + ) > 1) + ) { + throw Error(`googleMetadataJSON reading check failed ❌ , + for ${fileName} + expected: ${JSON.stringify( + metadata['location'] + )} + got: [${matchingFile.metadata.latitude},${ + matchingFile.metadata.longitude + }]`); + } + } + ); + console.log('googleMetadataJSON reading check passed ✅'); +} diff --git a/tests/zip-file-reading.test.ts b/tests/zip-file-reading.test.ts new file mode 100644 index 000000000..6d0ffffc2 --- /dev/null +++ b/tests/zip-file-reading.test.ts @@ -0,0 +1,105 @@ +import { DataStream } from 'types/upload'; +import ImportService from 'services/importService'; +import { FILE_READER_CHUNK_SIZE, PICKED_UPLOAD_TYPE } from 'constants/upload'; +import { getFileStream, getElectronFileStream } from 'services/readerService'; +import { getFileNameSize } from 'utils/logging'; +import isElectron from 'is-electron'; +import { getImportSuggestion } from 'utils/upload'; + +export const testZipFileReading = async () => { + try { + if (!isElectron()) { + console.log('testZipFileReading Check is for desktop only'); + return; + } + if (!process.env.NEXT_PUBLIC_FILE_READING_TEST_ZIP_PATH) { + throw Error( + 'upload test failed NEXT_PUBLIC_FILE_READING_TEST_ZIP_PATH missing' + ); + } + const files = await ImportService.getElectronFilesFromGoogleZip( + process.env.NEXT_PUBLIC_FILE_READING_TEST_ZIP_PATH + ); + if (!files?.length) { + throw Error( + `testZipFileReading Check failed ❌ + No files selected` + ); + } + console.log('test zip file reading check started'); + let i = 0; + for (const file of files) { + i++; + let filedata: DataStream; + if (file instanceof File) { + filedata = getFileStream(file, FILE_READER_CHUNK_SIZE); + } else { + filedata = await getElectronFileStream( + file, + FILE_READER_CHUNK_SIZE + ); + } + const streamReader = filedata.stream.getReader(); + for (let i = 0; i < filedata.chunkCount; i++) { + const { done } = await streamReader.read(); + if (done) { + throw Error( + `testZipFileReading Check failed ❌ + ${getFileNameSize( + file + )} less than expected chunks, expected: ${ + filedata.chunkCount + }, got ${i - 1}` + ); + } + } + const { done } = await streamReader.read(); + + if (!done) { + throw Error( + `testZipFileReading Check failed ❌ + ${getFileNameSize( + file + )} more than expected chunks, expected: ${ + filedata.chunkCount + }` + ); + } + console.log(`${i}/${files.length} passed ✅`); + } + console.log('test zip file reading check passed ✅'); + } catch (e) { + console.log(e); + } +}; + +export const testZipWithRootFileReadingTest = async () => { + try { + if (!isElectron()) { + console.log('testZipFileReading Check is for desktop only'); + return; + } + if (!process.env.NEXT_PUBLIC_ZIP_WITH_ROOT_FILE_PATH) { + throw Error( + 'upload test failed NEXT_PUBLIC_ZIP_WITH_ROOT_FILE_PATH missing' + ); + } + const files = await ImportService.getElectronFilesFromGoogleZip( + process.env.NEXT_PUBLIC_ZIP_WITH_ROOT_FILE_PATH + ); + + const importSuggestion = getImportSuggestion( + PICKED_UPLOAD_TYPE.ZIPS, + files + ); + if (!importSuggestion.rootFolderName) { + throw Error( + `testZipWithRootFileReadingTest Check failed ❌ + rootFolderName is missing` + ); + } + console.log('testZipWithRootFileReadingTest passed ✅'); + } catch (e) { + console.log(e); + } +}; diff --git a/tsconfig.json b/tsconfig.json index dce11bcd5..203589ff1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,12 @@ "lib": ["dom", "dom.iterable", "esnext", "webworker"], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, + "strictBindCallApply": false, + "strictNullChecks": false, + "strictPropertyInitialization": false, + "noImplicitAny": false, + "useUnknownInCatchVariables": false, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, @@ -20,12 +25,6 @@ "downlevelIteration": true, "incremental": true }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "src/pages/index.tsx", - "configUtil.js" - ], - "exclude": ["node_modules"] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], + "exclude": ["node_modules", "out", ".next", "thirdparty"] } diff --git a/yarn.lock b/yarn.lock index 6a957112d..0f7de7efe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,46 +2,13 @@ # yarn lockfile v1 -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.5.5": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.14.5": version "7.14.5" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz" integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== dependencies: "@babel/highlight" "^7.14.5" -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.15.0": - version "7.15.0" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz" - integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA== - -"@babel/core@^7.8.4": - version "7.15.5" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.15.5.tgz" - integrity sha512-pYgXxiwAgQpgM1bNkZsDEq85f0ggXMA5L7c+o3tskGMh2BunCI9QUwB9Z4jpvXUOuMdyGKiGKQiRe11VS6Jzvg== - dependencies: - "@babel/code-frame" "^7.14.5" - "@babel/generator" "^7.15.4" - "@babel/helper-compilation-targets" "^7.15.4" - "@babel/helper-module-transforms" "^7.15.4" - "@babel/helpers" "^7.15.4" - "@babel/parser" "^7.15.5" - "@babel/template" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.4" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" - source-map "^0.5.0" - "@babel/generator@^7.15.4": version "7.15.4" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz" @@ -51,73 +18,14 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.14.5", "@babel/helper-annotate-as-pure@^7.15.4": +"@babel/helper-annotate-as-pure@^7.0.0": version "7.15.4" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz" integrity sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA== dependencies: "@babel/types" "^7.15.4" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.14.5": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.15.4.tgz" - integrity sha512-P8o7JP2Mzi0SdC6eWr1zF+AEYvrsZa7GSY1lTayjF5XJhVH0kjLYUZPvTMflP7tBgZoe9gIhTa60QwFpqh/E0Q== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.15.4" - "@babel/types" "^7.15.4" - -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz" - integrity sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ== - dependencies: - "@babel/compat-data" "^7.15.0" - "@babel/helper-validator-option" "^7.14.5" - browserslist "^4.16.6" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.14.5", "@babel/helper-create-class-features-plugin@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.4.tgz" - integrity sha512-7ZmzFi+DwJx6A7mHRwbuucEYpyBwmh2Ca0RvI6z2+WLZYCqV0JOaLb+u0zbtmDicebgKBZgqbYfLaKNqSgv5Pw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.15.4" - "@babel/helper-function-name" "^7.15.4" - "@babel/helper-member-expression-to-functions" "^7.15.4" - "@babel/helper-optimise-call-expression" "^7.15.4" - "@babel/helper-replace-supers" "^7.15.4" - "@babel/helper-split-export-declaration" "^7.15.4" - -"@babel/helper-create-regexp-features-plugin@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz" - integrity sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A== - dependencies: - "@babel/helper-annotate-as-pure" "^7.14.5" - regexpu-core "^4.7.1" - -"@babel/helper-define-polyfill-provider@^0.2.2": - version "0.2.3" - resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz" - integrity sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew== - dependencies: - "@babel/helper-compilation-targets" "^7.13.0" - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/traverse" "^7.13.0" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-explode-assignable-expression@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.15.4.tgz" - integrity sha512-J14f/vq8+hdC2KoWLIQSsGrC9EFBKE4NFts8pfMpymfApds+fPqR30AOUWc4tyr56h9l/GA1Sxv2q3dLZWbQ/g== - dependencies: - "@babel/types" "^7.15.4" - -"@babel/helper-function-name@^7.14.5", "@babel/helper-function-name@^7.15.4": +"@babel/helper-function-name@^7.15.4": version "7.15.4" resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz" integrity sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw== @@ -140,84 +48,13 @@ dependencies: "@babel/types" "^7.15.4" -"@babel/helper-member-expression-to-functions@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz" - integrity sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA== - dependencies: - "@babel/types" "^7.15.4" - -"@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.15.4": +"@babel/helper-module-imports@^7.0.0": version "7.15.4" resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz" integrity sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA== dependencies: "@babel/types" "^7.15.4" -"@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz" - integrity sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw== - dependencies: - "@babel/helper-module-imports" "^7.15.4" - "@babel/helper-replace-supers" "^7.15.4" - "@babel/helper-simple-access" "^7.15.4" - "@babel/helper-split-export-declaration" "^7.15.4" - "@babel/helper-validator-identifier" "^7.14.9" - "@babel/template" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.4" - -"@babel/helper-optimise-call-expression@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz" - integrity sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw== - dependencies: - "@babel/types" "^7.15.4" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-plugin-utils@^7.14.5": - version "7.16.7" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz" - integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== - -"@babel/helper-remap-async-to-generator@^7.14.5", "@babel/helper-remap-async-to-generator@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.15.4.tgz" - integrity sha512-v53MxgvMK/HCwckJ1bZrq6dNKlmwlyRNYM6ypaRTdXWGOE2c1/SCa6dL/HimhPulGhZKw9W0QhREM583F/t0vQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.15.4" - "@babel/helper-wrap-function" "^7.15.4" - "@babel/types" "^7.15.4" - -"@babel/helper-replace-supers@^7.14.5", "@babel/helper-replace-supers@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz" - integrity sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.15.4" - "@babel/helper-optimise-call-expression" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.4" - -"@babel/helper-simple-access@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz" - integrity sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg== - dependencies: - "@babel/types" "^7.15.4" - -"@babel/helper-skip-transparent-expression-wrappers@^7.14.5", "@babel/helper-skip-transparent-expression-wrappers@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz" - integrity sha512-BMRLsdh+D1/aap19TycS4eD1qELGrCBJwzaY9IE8LrpJtJb+H7rQkPIdsfgnMtLBA6DJls7X9z93Z4U8h7xw0A== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-split-export-declaration@^7.15.4": version "7.15.4" resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz" @@ -225,44 +62,11 @@ dependencies: "@babel/types" "^7.15.4" -"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.16.7": +"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9": version "7.16.7" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz" integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== -"@babel/helper-validator-option@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz" - integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== - -"@babel/helper-wrap-function@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.15.4.tgz" - integrity sha512-Y2o+H/hRV5W8QhIfTpRIBwl57y8PrZt6JM3V8FOo5qarjshHItyH5lXlpMfBfmBefOqSCpKZs/6Dxqp0E/U+uw== - dependencies: - "@babel/helper-function-name" "^7.15.4" - "@babel/template" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.4" - -"@babel/helpers@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz" - integrity sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ== - dependencies: - "@babel/template" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.4" - -"@babel/highlight@^7.10.4": - version "7.16.10" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz" - integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@babel/highlight@^7.14.5": version "7.14.5" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz" @@ -272,593 +76,12 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.15.4", "@babel/parser@^7.15.5": +"@babel/parser@^7.15.4": version "7.15.6" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.15.6.tgz" integrity sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q== -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.15.4.tgz" - integrity sha512-eBnpsl9tlhPhpI10kU06JHnrYXwg3+V6CaP2idsCXNef0aeslpqyITXQ74Vfk5uHgY7IG7XP0yIH8b42KSzHog== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.15.4" - "@babel/plugin-proposal-optional-chaining" "^7.14.5" - -"@babel/plugin-proposal-async-generator-functions@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.15.4.tgz" - integrity sha512-2zt2g5vTXpMC3OmK6uyjvdXptbhBXfA77XGrd3gh93zwG8lZYBLOBImiGBEG0RANu3JqKEACCz5CGk73OJROBw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.15.4" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz" - integrity sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-proposal-class-static-block@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.15.4.tgz" - integrity sha512-M682XWrrLNk3chXCjoPUQWOyYsB93B9z3mRyjtqqYJWDf2mfCdIYgDrA11cgNVhAQieaq6F2fn2f3wI0U4aTjA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.15.4" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz" - integrity sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz" - integrity sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz" - integrity sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz" - integrity sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz" - integrity sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz" - integrity sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.15.6": - version "7.15.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.15.6.tgz" - integrity sha512-qtOHo7A1Vt+O23qEAX+GdBpqaIuD3i9VRrWgCJeq7WO6H2d14EK3q11urj5Te2MAeK97nMiIdRpwd/ST4JFbNg== - dependencies: - "@babel/compat-data" "^7.15.0" - "@babel/helper-compilation-targets" "^7.15.4" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.15.4" - -"@babel/plugin-proposal-optional-catch-binding@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz" - integrity sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz" - integrity sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz" - integrity sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-proposal-private-property-in-object@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.15.4.tgz" - integrity sha512-X0UTixkLf0PCCffxgu5/1RQyGGbgZuKoI+vXP4iSbJSYwPb7hu06omsFGBvQ9lJEvwgrxHdS8B5nbfcd8GyUNA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.15.4" - "@babel/helper-create-class-features-plugin" "^7.15.4" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.14.5", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz" - integrity sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-arrow-functions@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz" - integrity sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-async-to-generator@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz" - integrity sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA== - dependencies: - "@babel/helper-module-imports" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.14.5" - -"@babel/plugin-transform-block-scoped-functions@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz" - integrity sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-block-scoping@^7.15.3": - version "7.15.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz" - integrity sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-classes@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.15.4.tgz" - integrity sha512-Yjvhex8GzBmmPQUvpXRPWQ9WnxXgAFuZSrqOK/eJlOGIXwvv8H3UEdUigl1gb/bnjTrln+e8bkZUYCBt/xYlBg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.15.4" - "@babel/helper-function-name" "^7.15.4" - "@babel/helper-optimise-call-expression" "^7.15.4" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.15.4" - "@babel/helper-split-export-declaration" "^7.15.4" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz" - integrity sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-destructuring@^7.14.7": - version "7.14.7" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz" - integrity sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-dotall-regex@^7.14.5", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz" - integrity sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-duplicate-keys@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz" - integrity sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-exponentiation-operator@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz" - integrity sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-for-of@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.15.4.tgz" - integrity sha512-DRTY9fA751AFBDh2oxydvVm4SYevs5ILTWLs6xKXps4Re/KG5nfUkr+TdHCrRWB8C69TlzVgA9b3RmGWmgN9LA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-function-name@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz" - integrity sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ== - dependencies: - "@babel/helper-function-name" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-literals@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz" - integrity sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-member-expression-literals@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz" - integrity sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-modules-amd@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz" - integrity sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g== - dependencies: - "@babel/helper-module-transforms" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz" - integrity sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA== - dependencies: - "@babel/helper-module-transforms" "^7.15.4" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-simple-access" "^7.15.4" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.15.4.tgz" - integrity sha512-fJUnlQrl/mezMneR72CKCgtOoahqGJNVKpompKwzv3BrEXdlPspTcyxrZ1XmDTIr9PpULrgEQo3qNKp6dW7ssw== - dependencies: - "@babel/helper-hoist-variables" "^7.15.4" - "@babel/helper-module-transforms" "^7.15.4" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-identifier" "^7.14.9" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz" - integrity sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA== - dependencies: - "@babel/helper-module-transforms" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.14.9": - version "7.14.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz" - integrity sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.14.5" - -"@babel/plugin-transform-new-target@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz" - integrity sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-object-super@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz" - integrity sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.14.5" - -"@babel/plugin-transform-parameters@^7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.15.4.tgz" - integrity sha512-9WB/GUTO6lvJU3XQsSr6J/WKvBC2hcs4Pew8YxZagi6GkTdniyqp8On5kqdK8MN0LMeu0mGbhPN+O049NV/9FQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-property-literals@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz" - integrity sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-regenerator@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz" - integrity sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg== - dependencies: - regenerator-transform "^0.14.2" - -"@babel/plugin-transform-reserved-words@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz" - integrity sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-shorthand-properties@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz" - integrity sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-spread@^7.14.6": - version "7.14.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz" - integrity sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" - -"@babel/plugin-transform-sticky-regex@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz" - integrity sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-template-literals@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz" - integrity sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-typeof-symbol@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz" - integrity sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-unicode-escapes@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz" - integrity sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-unicode-regex@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz" - integrity sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/preset-env@^7.8.4": - version "7.15.6" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.15.6.tgz" - integrity sha512-L+6jcGn7EWu7zqaO2uoTDjjMBW+88FXzV8KvrBl2z6MtRNxlsmUNRlZPaNNPUTgqhyC5DHNFk/2Jmra+ublZWw== - dependencies: - "@babel/compat-data" "^7.15.0" - "@babel/helper-compilation-targets" "^7.15.4" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-option" "^7.14.5" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.15.4" - "@babel/plugin-proposal-async-generator-functions" "^7.15.4" - "@babel/plugin-proposal-class-properties" "^7.14.5" - "@babel/plugin-proposal-class-static-block" "^7.15.4" - "@babel/plugin-proposal-dynamic-import" "^7.14.5" - "@babel/plugin-proposal-export-namespace-from" "^7.14.5" - "@babel/plugin-proposal-json-strings" "^7.14.5" - "@babel/plugin-proposal-logical-assignment-operators" "^7.14.5" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5" - "@babel/plugin-proposal-numeric-separator" "^7.14.5" - "@babel/plugin-proposal-object-rest-spread" "^7.15.6" - "@babel/plugin-proposal-optional-catch-binding" "^7.14.5" - "@babel/plugin-proposal-optional-chaining" "^7.14.5" - "@babel/plugin-proposal-private-methods" "^7.14.5" - "@babel/plugin-proposal-private-property-in-object" "^7.15.4" - "@babel/plugin-proposal-unicode-property-regex" "^7.14.5" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.14.5" - "@babel/plugin-transform-async-to-generator" "^7.14.5" - "@babel/plugin-transform-block-scoped-functions" "^7.14.5" - "@babel/plugin-transform-block-scoping" "^7.15.3" - "@babel/plugin-transform-classes" "^7.15.4" - "@babel/plugin-transform-computed-properties" "^7.14.5" - "@babel/plugin-transform-destructuring" "^7.14.7" - "@babel/plugin-transform-dotall-regex" "^7.14.5" - "@babel/plugin-transform-duplicate-keys" "^7.14.5" - "@babel/plugin-transform-exponentiation-operator" "^7.14.5" - "@babel/plugin-transform-for-of" "^7.15.4" - "@babel/plugin-transform-function-name" "^7.14.5" - "@babel/plugin-transform-literals" "^7.14.5" - "@babel/plugin-transform-member-expression-literals" "^7.14.5" - "@babel/plugin-transform-modules-amd" "^7.14.5" - "@babel/plugin-transform-modules-commonjs" "^7.15.4" - "@babel/plugin-transform-modules-systemjs" "^7.15.4" - "@babel/plugin-transform-modules-umd" "^7.14.5" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.14.9" - "@babel/plugin-transform-new-target" "^7.14.5" - "@babel/plugin-transform-object-super" "^7.14.5" - "@babel/plugin-transform-parameters" "^7.15.4" - "@babel/plugin-transform-property-literals" "^7.14.5" - "@babel/plugin-transform-regenerator" "^7.14.5" - "@babel/plugin-transform-reserved-words" "^7.14.5" - "@babel/plugin-transform-shorthand-properties" "^7.14.5" - "@babel/plugin-transform-spread" "^7.14.6" - "@babel/plugin-transform-sticky-regex" "^7.14.5" - "@babel/plugin-transform-template-literals" "^7.14.5" - "@babel/plugin-transform-typeof-symbol" "^7.14.5" - "@babel/plugin-transform-unicode-escapes" "^7.14.5" - "@babel/plugin-transform-unicode-regex" "^7.14.5" - "@babel/preset-modules" "^0.1.4" - "@babel/types" "^7.15.6" - babel-plugin-polyfill-corejs2 "^0.2.2" - babel-plugin-polyfill-corejs3 "^0.2.2" - babel-plugin-polyfill-regenerator "^0.2.2" - core-js-compat "^3.16.0" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.4": - version "0.1.4" - resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz" - integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/runtime-corejs3@^7.10.2": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.15.4.tgz" - integrity sha512-lWcAqKeB624/twtTc3w6w/2o9RqJPaNBhPGK6DKLSiwuVWC7WFkypWyNg+CpZoyJH0jVzv1uMtXZ/5/lQOLtCg== - dependencies: - core-js-pure "^3.16.0" - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": version "7.15.4" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz" integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== @@ -879,6 +102,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" + integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/template@^7.15.4": version "7.15.4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz" @@ -888,7 +118,7 @@ "@babel/parser" "^7.15.4" "@babel/types" "^7.15.4" -"@babel/traverse@^7.13.0", "@babel/traverse@^7.15.4", "@babel/traverse@^7.4.5": +"@babel/traverse@^7.4.5": version "7.15.4" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz" integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA== @@ -903,7 +133,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.15.4", "@babel/types@^7.15.6", "@babel/types@^7.4.4": +"@babel/types@^7.15.4": version "7.15.6" resolved "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz" integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig== @@ -1033,75 +263,39 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@ente-io/next-with-workbox@^1.0.3": - version "1.0.3" - resolved "https://registry.npmjs.org/@ente-io/next-with-workbox/-/next-with-workbox-1.0.3.tgz" - integrity sha512-+OAiY75RWj+15MqE64JVRUP7UZGONhTsNIx16p9NIAIvatn5ds4132o69mB3uj5eQQGs1eU2EIlpCi+3nfOJcQ== - dependencies: - clean-webpack-plugin "^3.0.0" - glob "^7.1.6" - workbox-webpack-plugin "^5.1.4" - -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== +"@eslint/eslintrc@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" + integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@hapi/address@2.x.x": - version "2.1.4" - resolved "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz" - integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== - -"@hapi/bourne@1.x.x": - version "1.3.2" - resolved "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz" - integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA== - -"@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0": - version "8.5.1" - resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz" - integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== - -"@hapi/joi@^15.1.0": - version "15.1.1" - resolved "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz" - integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ== +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== dependencies: - "@hapi/address" "2.x.x" - "@hapi/bourne" "1.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/topo" "3.x.x" - -"@hapi/topo@3.x.x": - version "3.1.6" - resolved "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz" - integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== - dependencies: - "@hapi/hoek" "^8.3.0" - -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== - dependencies: - "@humanwhocodes/object-schema" "^1.2.0" + "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" - minimatch "^3.0.4" + minimatch "^3.0.5" -"@humanwhocodes/object-schema@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz" - integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@mui/base@5.0.0-alpha.77": version "5.0.0-alpha.77" @@ -1223,65 +417,82 @@ dependencies: webpack-bundle-analyzer "3.6.1" -"@next/env@12.1.0": - version "12.1.0" - resolved "https://registry.npmjs.org/@next/env/-/env-12.1.0.tgz" - integrity sha512-nrIgY6t17FQ9xxwH3jj0a6EOiQ/WDHUos35Hghtr+SWN/ntHIQ7UpuvSi0vaLzZVHQWaDupKI+liO5vANcDeTQ== +"@next/env@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.1.2.tgz#4f13e3e9d44bb17fdc1d4543827459097035f10f" + integrity sha512-PpT4UZIX66VMTqXt4HKEJ+/PwbS+tWmmhZlazaws1a+dbUA5pPdjntQ46Jvj616i3ZKN9doS9LHx3y50RLjAWg== -"@next/swc-android-arm64@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39" - integrity sha512-/280MLdZe0W03stA69iL+v6I+J1ascrQ6FrXBlXGCsGzrfMaGr7fskMa0T5AhQIVQD4nA/46QQWxG//DYuFBcA== +"@next/eslint-plugin-next@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.1.2.tgz#613deefddc9a2f3a3ef01a100cbcff54dfa03fd0" + integrity sha512-WGaNVvIYphdriesP6r7jq/8l7u38tzotnVQuxc1RYKLqYYApSsrebti3OCPoT3Gx0pw2smPIFHH98RzcsgW5GQ== + dependencies: + glob "7.1.7" -"@next/swc-darwin-arm64@12.1.0": - version "12.1.0" - resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.0.tgz" - integrity sha512-R8vcXE2/iONJ1Unf5Ptqjk6LRW3bggH+8drNkkzH4FLEQkHtELhvcmJwkXcuipyQCsIakldAXhRbZmm3YN1vXg== +"@next/swc-android-arm-eabi@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.2.tgz#eacc7757b480a8150c1aea748bf7892a4fc62f64" + integrity sha512-7mRz1owoGsbfIcdOJA3kk7KEwPZ+OvVT1z9DkR/yru4QdVLF69h/1SHy0vlUNQMxDRllabhxCfkoZCB34GOGAg== -"@next/swc-darwin-x64@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.0.tgz#fcd684497a76e8feaca88db3c394480ff0b007cd" - integrity sha512-ieAz0/J0PhmbZBB8+EA/JGdhRHBogF8BWaeqR7hwveb6SYEIJaDNQy0I+ZN8gF8hLj63bEDxJAs/cEhdnTq+ug== +"@next/swc-android-arm64@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.1.2.tgz#f3d41339b4f15852a589fe11820408572a512a27" + integrity sha512-mgjZ2eJSayovQm1LcE54BLSI4jjnnnLtq5GY5g+DdPuUiCT644gKtjZ/w2BQvuIecCqqBO+Ph9yzo/wUTq7NLg== -"@next/swc-linux-arm-gnueabihf@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.0.tgz#9ec6380a27938a5799aaa6035c205b3c478468a7" - integrity sha512-njUd9hpl6o6A5d08dC0cKAgXKCzm5fFtgGe6i0eko8IAdtAPbtHxtpre3VeSxdZvuGFh+hb0REySQP9T1ttkog== +"@next/swc-darwin-arm64@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.2.tgz#1a20a2262aa7a250517c9a7f2efd6ac6273f8c63" + integrity sha512-RikoQqy109r2222UJlyGs4dZw2BibkfPqpeFdW5JEGv+L2PStlHID8DwyVYbmHfQ0VIBGvbf/NAUtFakAWlhwg== -"@next/swc-linux-arm64-gnu@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.0.tgz#7f4196dff1049cea479607c75b81033ae2dbd093" - integrity sha512-OqangJLkRxVxMhDtcb7Qn1xjzFA3s50EIxY7mljbSCLybU+sByPaWAHY4px97ieOlr2y4S0xdPKkQ3BCAwyo6Q== +"@next/swc-darwin-x64@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.2.tgz#242bb321676bd88f4cffa7eae3283215cd1185ce" + integrity sha512-JbDZjaTvL8gyPC5TAH6OnD4jmXPkyUxRYPvu08ZmhT/XAFBb/Cso0BdXyDax/BPCG70mimP9d3hXNKNq+A0VtQ== -"@next/swc-linux-arm64-musl@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.0.tgz#b445f767569cdc2dddee785ca495e1a88c025566" - integrity sha512-hB8cLSt4GdmOpcwRe2UzI5UWn6HHO/vLkr5OTuNvCJ5xGDwpPXelVkYW/0+C3g5axbDW2Tym4S+MQCkkH9QfWA== +"@next/swc-freebsd-x64@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.2.tgz#9589f7f2bebfa43a744c9e41654e743b38a318b1" + integrity sha512-ax4j8VrdFQ/xc3W7Om0u1vnDxVApQHKsChBbAMynCrnycZmpbqK4MZu4ZkycT+mx2eccCiqZROpbzDbEdPosEw== -"@next/swc-linux-x64-gnu@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.0.tgz#67610e9be4fbc987de7535f1bcb17e45fe12f90e" - integrity sha512-OKO4R/digvrVuweSw/uBM4nSdyzsBV5EwkUeeG4KVpkIZEe64ZwRpnFB65bC6hGwxIBnTv5NMSnJ+0K/WmG78A== +"@next/swc-linux-arm-gnueabihf@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.2.tgz#8935b0c8f232e36c3d88cd1e1023afa8d51f7260" + integrity sha512-NcRHTesnCxnUvSJa637PQJffBBkmqi5XS/xVWGY7dI6nyJ+pC96Oj7kd+mcjnFUQI5lHKbg39qBWKtOzbezc4w== -"@next/swc-linux-x64-musl@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.0.tgz#ea19a23db08a9f2e34ac30401f774cf7d1669d31" - integrity sha512-JohhgAHZvOD3rQY7tlp7NlmvtvYHBYgY0x5ZCecUT6eCCcl9lv6iV3nfu82ErkxNk1H893fqH0FUpznZ/H3pSw== +"@next/swc-linux-arm64-gnu@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.2.tgz#3482f72e580cdfc4bbec2e55dd55d5a9bdf7038b" + integrity sha512-AxJdjocLtPrsBY4P2COSBIc3crT5bpjgGenNuINoensOlXhBkYM0aRDYZdydwXOhG+kN2ngUvfgitop9pa204w== -"@next/swc-win32-arm64-msvc@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.0.tgz#eadf054fc412085659b98e145435bbba200b5283" - integrity sha512-T/3gIE6QEfKIJ4dmJk75v9hhNiYZhQYAoYm4iVo1TgcsuaKLFa+zMPh4056AHiG6n9tn2UQ1CFE8EoybEsqsSw== +"@next/swc-linux-arm64-musl@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.2.tgz#3b7ca70fd813c77f618ee34a150b977cc15af9a3" + integrity sha512-JmNimDkcCRq7P5zpkdqeaSZ69qKDntEPtyIaMNWqy5M0WUJxGim0Fs6Qzxayiyvuuh9Guxks4woQ/j/ZvX/c8Q== -"@next/swc-win32-ia32-msvc@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.0.tgz#68faeae10c89f698bf9d28759172b74c9c21bda1" - integrity sha512-iwnKgHJdqhIW19H9PRPM9j55V6RdcOo6rX+5imx832BCWzkDbyomWnlzBfr6ByUYfhohb8QuH4hSGEikpPqI0Q== +"@next/swc-linux-x64-gnu@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.2.tgz#51a7a889e88eb87a5ce9658842f9e8422e037ead" + integrity sha512-TsLsjZwUlgmvI42neTuIoD6K9RlXCUzqPtvIClgXxVO0um0DiZwK+M+0zX/uVXhMVphfPY2c5YeR1zFSIONY4A== -"@next/swc-win32-x64-msvc@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.0.tgz#d27e7e76c87a460a4da99c5bfdb1618dcd6cd064" - integrity sha512-aBvcbMwuanDH4EMrL2TthNJy+4nP59Bimn8egqv6GHMVj0a44cU6Au4PjOhLNqEh9l+IpRGBqMTzec94UdC5xg== +"@next/swc-linux-x64-musl@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.1.2.tgz#4c0dd08a6f8a7e4881c3551de29259b3cfe86e27" + integrity sha512-eSkyXgCXydEFPTkcncQOGepafedPte6JT/OofB9uvruucrrMVBagCASOuPxodWEMrlfEKSXVnExMKIlfmQMD7A== + +"@next/swc-win32-arm64-msvc@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.2.tgz#589fcce82f9f7224d2399d8d7bcba9097bb50dad" + integrity sha512-DmXFaRTgt2KrV9dmRLifDJE+cYiutHVFIw5/C9BtnwXH39uf3YbPxeD98vNrtqqqZVVLXY/1ySaSIwzYnqeY9g== + +"@next/swc-win32-ia32-msvc@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.2.tgz#9be05202730530631b51d7753d447dfe86095c9f" + integrity sha512-3+nBkuFs/wT+lmRVQNH5SyDT7I4vUlNPntosEaEP63FuYQdPLaxz0GvcR66MdFSFh2fsvazpe4wciOwVS4FItQ== + +"@next/swc-win32-x64-msvc@13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.2.tgz#c7e75033e8b8f497768c7b462ac642830141bb00" + integrity sha512-avsyveEvcvH42PvKjR4Pb8JlLttuGURr2H3ZhS2b85pHOiZ7yjH3rMUoGnNzuLMApyxYaCvd4MedPrLhnNhkog== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1296,7 +507,7 @@ resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -1304,6 +515,18 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pkgr/utils@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03" + integrity sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw== + dependencies: + cross-spawn "^7.0.3" + is-glob "^4.0.3" + open "^8.4.0" + picocolors "^1.0.0" + tiny-glob "^0.2.9" + tslib "^2.4.0" + "@popperjs/core@^2.11.5": version "2.11.5" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" @@ -1331,33 +554,10 @@ dependencies: dequal "^2.0.2" -"@rollup/plugin-node-resolve@^7.1.1": - version "7.1.3" - resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz" - integrity sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q== - dependencies: - "@rollup/pluginutils" "^3.0.8" - "@types/resolve" "0.0.8" - builtin-modules "^3.1.0" - is-module "^1.0.0" - resolve "^1.14.2" - -"@rollup/plugin-replace@^2.3.1": - version "2.4.2" - resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz" - integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - magic-string "^0.25.7" - -"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" - integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== - dependencies: - "@types/estree" "0.0.39" - estree-walker "^1.0.1" - picomatch "^2.2.2" +"@rushstack/eslint-patch@^1.1.3": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" + integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== "@sentry/browser@6.12.0": version "6.12.0" @@ -1497,13 +697,12 @@ resolved "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.17.1.tgz" integrity sha512-c9MyDvdi5Xou0j0JPNy86NebtTDfh9o62Ifuzx6GSm2YO0oedBpy51WSyOue2L8Fb+mqESS5gd6mGVEIPUnXsA== -"@surma/rollup-plugin-off-main-thread@^1.1.1": - version "1.4.2" - resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.2.tgz" - integrity sha512-yBMPqmd1yEJo/280PAMkychuaALyQ9Lkb5q1ck3mjJrFuEobIfhnQ4J3mbvBoISmR3SWMWV+cGB/I0lCQee79A== +"@swc/helpers@0.4.14": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74" + integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw== dependencies: - ejs "^2.6.1" - magic-string "^0.25.0" + tslib "^2.4.0" "@tensorflow-models/coco-ssd@^2.2.2": version "2.2.2" @@ -1566,6 +765,13 @@ resolved "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz" integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== +"@types/bs58@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/bs58/-/bs58-4.0.1.tgz#3d51222aab067786d3bc3740a84a7f5a0effaa37" + integrity sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA== + dependencies: + base-x "^3.0.6" + "@types/d3-hierarchy@^1.1.8": version "1.1.8" resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz#50657f420d565a06c0b950a4b82eee0a369f2dea" @@ -1576,24 +782,6 @@ resolved "https://registry.npmjs.org/@types/debounce-promise/-/debounce-promise-3.1.4.tgz" integrity sha512-9SEVY3nsz+uMN2DwDocftB5TAgZe7D0cOzxxRhpotWs6T4QFqRaTXpXbOSzbk31/7iYcfCkJJPwWGzTxyuGhCg== -"@types/estree@*": - version "0.0.50" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== - -"@types/estree@0.0.39": - version "0.0.39" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - -"@types/glob@^7.1.1": - version "7.1.4" - resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz" - integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - "@types/hoist-non-react-statics@*": version "3.3.1" resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" @@ -1614,10 +802,10 @@ resolved "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz" integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== -"@types/json-schema@^7.0.7": - version "7.0.9" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/json5@^0.0.29": version "0.0.29" @@ -1634,11 +822,6 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== -"@types/minimatch@*": - version "3.0.5" - resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz" - integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== - "@types/node@*": version "17.0.18" resolved "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz" @@ -1767,13 +950,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/resolve@0.0.8": - version "0.0.8" - resolved "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz" - integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== - dependencies: - "@types/node" "*" - "@types/scheduler@*": version "0.16.2" resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" @@ -1784,10 +960,10 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.27.tgz#9db563937dd86915f69092bc43259d2f48578e41" integrity sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE= -"@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== +"@types/semver@^7.3.12": + version "7.3.13" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== "@types/styled-components@^5.1.25": version "5.1.25" @@ -1798,23 +974,11 @@ "@types/react" "*" csstype "^3.0.2" -"@types/tapable@^1": - version "1.0.8" - resolved "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz" - integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== - "@types/trusted-types@^2.0.2": version "2.0.2" resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz" integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== -"@types/uglify-js@*": - version "3.13.1" - resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz" - integrity sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ== - dependencies: - source-map "^0.6.1" - "@types/warning@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz" @@ -1830,27 +994,6 @@ resolved "https://registry.yarnpkg.com/@types/webgl2/-/webgl2-0.0.6.tgz#1ea2db791362bd8521548d664dbd3c5311cdf4b6" integrity sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ== -"@types/webpack-sources@*": - version "3.2.0" - resolved "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz" - integrity sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg== - dependencies: - "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.7.3" - -"@types/webpack@^4.4.31": - version "4.41.30" - resolved "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.30.tgz" - integrity sha512-GUHyY+pfuQ6haAfzu4S14F+R5iGRwN6b2FRNJY7U0NilmFAqbsOfK6j1HwuLBAqwRIT+pVdNDJGJ6e8rpp0KHA== - dependencies: - "@types/node" "*" - "@types/tapable" "^1" - "@types/uglify-js" "*" - "@types/webpack-sources" "*" - anymatch "^3.0.0" - source-map "^0.6.0" - "@types/wicg-file-system-access@^2020.9.5": version "2020.9.5" resolved "https://registry.yarnpkg.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz#4a0c8f3d1ed101525f329e86c978f7735404474f" @@ -1861,74 +1004,88 @@ resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.13.tgz" integrity sha512-qRyuv+P/1t1JK1rA+elmK1MmCL1BapEzKKfbEhDBV/LMMse4lmhZ/XbgETI39JveDJRpLjmToOI6uFtMW/WR2g== -"@typescript-eslint/eslint-plugin@^4.25.0": - version "4.31.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.0.tgz" - integrity sha512-iPKZTZNavAlOhfF4gymiSuUkgLne/nh5Oz2/mdiUmuZVD42m9PapnCnzjxuDsnpnbH3wT5s2D8bw6S39TC6GNw== +"@typescript-eslint/eslint-plugin@^5.43.0": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz#deee67e399f2cb6b4608c935777110e509d8018c" + integrity sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ== dependencies: - "@typescript-eslint/experimental-utils" "4.31.0" - "@typescript-eslint/scope-manager" "4.31.0" - debug "^4.3.1" - functional-red-black-tree "^1.0.1" - regexpp "^3.1.0" - semver "^7.3.5" + "@typescript-eslint/scope-manager" "5.48.1" + "@typescript-eslint/type-utils" "5.48.1" + "@typescript-eslint/utils" "5.48.1" + debug "^4.3.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + regexpp "^3.2.0" + semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.31.0": - version "4.31.0" - resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.0.tgz" - integrity sha512-Hld+EQiKLMppgKKkdUsLeVIeEOrwKc2G983NmznY/r5/ZtZCDvIOXnXtwqJIgYz/ymsy7n7RGvMyrzf1WaSQrw== +"@typescript-eslint/parser@^5.42.0": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.1.tgz#d0125792dab7e232035434ab8ef0658154db2f10" + integrity sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA== dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.31.0" - "@typescript-eslint/types" "4.31.0" - "@typescript-eslint/typescript-estree" "4.31.0" + "@typescript-eslint/scope-manager" "5.48.1" + "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/typescript-estree" "5.48.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz#39c71e4de639f5fe08b988005beaaf6d79f9d64d" + integrity sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ== + dependencies: + "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/visitor-keys" "5.48.1" + +"@typescript-eslint/type-utils@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz#5d94ac0c269a81a91ad77c03407cea2caf481412" + integrity sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ== + dependencies: + "@typescript-eslint/typescript-estree" "5.48.1" + "@typescript-eslint/utils" "5.48.1" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.1.tgz#efd1913a9aaf67caf8a6e6779fd53e14e8587e14" + integrity sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg== + +"@typescript-eslint/typescript-estree@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz#9efa8ee2aa471c6ab62e649f6e64d8d121bc2056" + integrity sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA== + dependencies: + "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/visitor-keys" "5.48.1" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.1.tgz#20f2f4e88e9e2a0961cbebcb47a1f0f7da7ba7f9" + integrity sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA== + dependencies: + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.48.1" + "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/typescript-estree" "5.48.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" + semver "^7.3.7" -"@typescript-eslint/parser@^4.25.0": - version "4.31.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.0.tgz" - integrity sha512-oWbzvPh5amMuTmKaf1wp0ySxPt2ZXHnFQBN2Szu1O//7LmOvgaKTCIDNLK2NvzpmVd5A2M/1j/rujBqO37hj3w== +"@typescript-eslint/visitor-keys@5.48.1": + version "5.48.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz#79fd4fb9996023ef86849bf6f904f33eb6c8fccb" + integrity sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA== dependencies: - "@typescript-eslint/scope-manager" "4.31.0" - "@typescript-eslint/types" "4.31.0" - "@typescript-eslint/typescript-estree" "4.31.0" - debug "^4.3.1" - -"@typescript-eslint/scope-manager@4.31.0": - version "4.31.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.0.tgz" - integrity sha512-LJ+xtl34W76JMRLjbaQorhR0hfRAlp3Lscdiz9NeI/8i+q0hdBZ7BsiYieLoYWqy+AnRigaD3hUwPFugSzdocg== - dependencies: - "@typescript-eslint/types" "4.31.0" - "@typescript-eslint/visitor-keys" "4.31.0" - -"@typescript-eslint/types@4.31.0": - version "4.31.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.0.tgz" - integrity sha512-9XR5q9mk7DCXgXLS7REIVs+BaAswfdHhx91XqlJklmqWpTALGjygWVIb/UnLh4NWhfwhR5wNe1yTyCInxVhLqQ== - -"@typescript-eslint/typescript-estree@4.31.0": - version "4.31.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.0.tgz" - integrity sha512-QHl2014t3ptg+xpmOSSPn5hm4mY8D4s97ftzyk9BZ8RxYQ3j73XcwuijnJ9cMa6DO4aLXeo8XS3z1omT9LA/Eg== - dependencies: - "@typescript-eslint/types" "4.31.0" - "@typescript-eslint/visitor-keys" "4.31.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/visitor-keys@4.31.0": - version "4.31.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.0.tgz" - integrity sha512-HUcRp2a9I+P21+O21yu3ezv3GEPGjyGiXoEUQwZXjR8UxRApGeLyWH4ZIIUSalE28aG4YsV6GjtaAVB3QKOu0w== - dependencies: - "@typescript-eslint/types" "4.31.0" - eslint-visitor-keys "^2.0.0" + "@typescript-eslint/types" "5.48.1" + eslint-visitor-keys "^3.3.0" "@zip.js/zip.js@^2.4.2": version "2.4.2" @@ -1943,9 +1100,9 @@ accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-jsx@^5.3.1: +acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^7.1.1: @@ -1953,11 +1110,16 @@ acorn-walk@^7.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0: +acorn@^7.1.1: version "7.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.8.0: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + agent-base@6: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" @@ -1983,16 +1145,6 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1: - version "8.6.2" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz" - integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -2029,9 +1181,9 @@ ansi-regex@^3.0.0: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^5.0.0: +ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.1: @@ -2048,14 +1200,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -anymatch@^3.0.0: - version "3.1.2" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - aproba@^1.0.3: version "1.2.0" resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz" @@ -2069,27 +1213,24 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== +aria-query@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== dependencies: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" + deep-equal "^2.0.5" array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= -array-includes@^3.1.1, array-includes@^3.1.2, array-includes@^3.1.3: +array-includes@^3.1.2: version "3.1.3" resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz" integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== @@ -2100,41 +1241,52 @@ array-includes@^3.1.1, array-includes@^3.1.2, array-includes@^3.1.3: get-intrinsic "^1.1.1" is-string "^1.0.5" -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= +array-includes@^3.1.5, array-includes@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== dependencies: - array-uniq "^1.0.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array.prototype.flat@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz" - integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== +array.prototype.flat@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz" - integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q== +array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - function-bind "^1.1.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" + integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.1.3" ast-types-flow@^0.0.7: version "0.0.7" @@ -2156,10 +1308,15 @@ attr-accept@^2.2.1: resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz" integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== -axe-core@^4.0.2: - version "4.3.3" - resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.3.3.tgz" - integrity sha512-/lqqLAmuIPi79WYfRpy2i8z+x+vxU3zX2uAm0gs1q52qTuKwolOj1P8XbufpXcsydrpKx2yGn2wzAnxCMV86QA== +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +axe-core@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.2.tgz#6e566ab2a3d29e415f5115bc0fd2597a5eb3e5e3" + integrity sha512-b1WlTV8+XKLj9gZy2DZXgQiyDp9xkkoe2a6U6UbYccScq2wgH/YwCeI2/Jq2mgo0HzQxqJOjWZBLeA/mqsk5Mg== axios@^0.21.3: version "0.21.4" @@ -2168,50 +1325,14 @@ axios@^0.21.3: dependencies: follow-redirects "^1.14.0" -axobject-query@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== - -babel-extract-comments@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz" - integrity sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ== +axobject-query@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" + integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== dependencies: - babylon "^6.18.0" + deep-equal "^2.0.5" -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-polyfill-corejs2@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz" - integrity sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ== - dependencies: - "@babel/compat-data" "^7.13.11" - "@babel/helper-define-polyfill-provider" "^0.2.2" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.2.2: - version "0.2.4" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.4.tgz" - integrity sha512-z3HnJE5TY/j4EFEa/qpQMSbcUJZ5JQi+3UFjXzn6pQCmIKc5Ug5j98SuYyH+m4xQnvKlMDIW4plLfgyVnd0IcQ== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.2.2" - core-js-compat "^3.14.0" - -babel-plugin-polyfill-regenerator@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz" - integrity sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.2.2" - -"babel-plugin-styled-components@>= 1.12.0", babel-plugin-styled-components@^1.11.1: +"babel-plugin-styled-components@>= 1.12.0": version "1.13.2" resolved "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz" integrity sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA== @@ -2226,38 +1347,12 @@ babel-plugin-syntax-jsx@^6.18.0: resolved "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz" integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= -babel-plugin-syntax-object-rest-spread@^6.8.0: - version "6.13.0" - resolved "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz" - integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= - -babel-plugin-transform-object-rest-spread@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz" - integrity sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY= - dependencies: - babel-plugin-syntax-object-rest-spread "^6.8.0" - babel-runtime "^6.26.0" - -babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-x@^3.0.2: +base-x@^3.0.2, base-x@^3.0.6: version "3.0.9" resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz" integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== @@ -2340,17 +1435,6 @@ braces@^3.0.1, braces@^3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.16.6, browserslist@^4.17.0: - version "4.17.0" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.17.0.tgz" - integrity sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g== - dependencies: - caniuse-lite "^1.0.30001254" - colorette "^1.3.0" - electron-to-chromium "^1.3.830" - escalade "^3.1.1" - node-releases "^1.1.75" - bs58@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz" @@ -2363,11 +1447,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -builtin-modules@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz" - integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== - bytes@3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz" @@ -2396,15 +1475,10 @@ camelize@^1.0.0: resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= -caniuse-lite@^1.0.30001254: - version "1.0.30001255" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz" - integrity sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ== - -caniuse-lite@^1.0.30001283: - version "1.0.30001312" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz" - integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ== +caniuse-lite@^1.0.30001406: + version "1.0.30001444" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001444.tgz#c0a530776eb44d933b493de1d05346f2527b30fc" + integrity sha512-ecER9xgJQVMqcrxThKptsW0pPxSae8R2RB87LNa+ivW9ppNWRHEplXcDzkCOP4LYWGj8hunXLqaiC41iBATNyg== center-align@^0.1.1: version "0.1.3" @@ -2466,14 +1540,6 @@ clean-stack@^2.0.0: resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clean-webpack-plugin@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz" - integrity sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A== - dependencies: - "@types/webpack" "^4.4.31" - del "^4.1.1" - cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" @@ -2489,6 +1555,11 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -2537,7 +1608,7 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^1.2.2, colorette@^1.3.0: +colorette@^1.2.2: version "1.4.0" resolved "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz" integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== @@ -2552,7 +1623,7 @@ comlink@^4.3.0: resolved "https://registry.npmjs.org/comlink/-/comlink-4.3.1.tgz" integrity sha512-+YbhUdNrpBZggBAHWcgQMLPLH1KDF3wJpeqrCKieWQ8RL7atmgsgTQko1XEBK6PsecfopWNntopJ+ByYG1lRaA== -commander@^2.18.0, commander@^2.20.0: +commander@^2.18.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -2562,11 +1633,6 @@ commander@^7.2.0: resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -common-tags@^1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz" - integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2582,11 +1648,6 @@ concat-stream@~1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" -confusing-browser-globals@^1.0.10: - version "1.0.10" - resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz" - integrity sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA== - console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" @@ -2604,13 +1665,6 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@^1.7.0: - version "1.8.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" @@ -2626,24 +1680,6 @@ cookie@^0.4.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== -core-js-compat@^3.14.0, core-js-compat@^3.16.0: - version "3.17.3" - resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.17.3.tgz" - integrity sha512-+in61CKYs4hQERiADCJsdgewpdl/X0GhEX77pjKgbeibXviIt2oxEjTc8O2fqHX8mDdBrDvX8MYD/RYsBv4OiA== - dependencies: - browserslist "^4.17.0" - semver "7.0.0" - -core-js-pure@^3.16.0: - version "3.17.3" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.17.3.tgz" - integrity sha512-YusrqwiOTTn8058JDa0cv9unbXdIiIgcgI9gXso0ey4WgkFLd3lYlV9rp9n7nDCsYxXsMDTjA4m1h3T348mdlQ== - -core-js@^2.4.0: - version "2.6.12" - resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" @@ -2692,11 +1728,6 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-random-string@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz" - integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= - css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" @@ -2826,10 +1857,10 @@ d3-zoom@^1.8.3: d3-selection "1" d3-transition "1" -damerau-levenshtein@^1.0.6: - version "1.0.7" - resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz" - integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw== +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== date-fns@^2.0.1: version "2.25.0" @@ -2851,14 +1882,14 @@ debounce-promise@^3.1.2: resolved "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz" integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg== -debug@2.6.9, debug@^2.6.9: +debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.2" resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -2872,11 +1903,41 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decamelize@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +deep-equal@^2.0.5: + version "2.2.0" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" + integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== + dependencies: + call-bind "^1.0.2" + es-get-iterator "^1.1.2" + get-intrinsic "^1.1.3" + is-arguments "^1.1.1" + is-array-buffer "^3.0.1" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -2887,6 +1948,11 @@ deepmerge@^2.1.1: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" @@ -2894,18 +1960,13 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" -del@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/del/-/del-4.1.1.tgz" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== +define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" delegates@^1.0.0: version "1.0.0" @@ -2995,19 +2056,14 @@ ejs@^2.6.1: resolved "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -electron-to-chromium@^1.3.830: - version "1.3.835" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.835.tgz" - integrity sha512-rHQszGg2KLMqOWPNTpwCnlp7Kb85haJa8j089DJCreZueykoSN/in+EMlay3SSDMNKR4VGPvfskxofHV18xVJg== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-regex@^9.0.0: +emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== encodeurl@~1.0.2: @@ -3015,15 +2071,15 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -enhanced-resolve@^5.7.0: - version "5.9.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88" - integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow== +enhanced-resolve@^5.10.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" + integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5, enquirer@^2.3.6: +enquirer@^2.3.6: version "2.3.6" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -3037,7 +2093,7 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: +es-abstract@^1.18.0-next.2: version "1.19.1" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz" integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== @@ -3063,6 +2119,76 @@ es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" +es-abstract@^1.19.0, es-abstract@^1.20.4: + version "1.21.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" + integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.4" + is-array-buffer "^3.0.1" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + +es-get-iterator@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" @@ -3072,11 +2198,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" @@ -3113,33 +2234,25 @@ escodegen@~1.3.2: optionalDependencies: source-map "~0.1.33" -eslint-config-airbnb-base@^14.2.1: - version "14.2.1" - resolved "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz" - integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== +eslint-config-next@^13.0.6: + version "13.1.2" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.1.2.tgz#bd15be592546e3543f77f96bc55be830dd76fd41" + integrity sha512-zdRAQOr8v69ZwJRtBrGqAqm160ONqKxU/pV1FB1KlgfyqveGsLZmlQ7l31otwtw763901J7xdiTVkj2y3YxXZA== dependencies: - confusing-browser-globals "^1.0.10" - object.assign "^4.1.2" - object.entries "^1.1.2" + "@next/eslint-plugin-next" "13.1.2" + "@rushstack/eslint-patch" "^1.1.3" + "@typescript-eslint/parser" "^5.42.0" + eslint-import-resolver-node "^0.3.6" + eslint-import-resolver-typescript "^3.5.2" + eslint-plugin-import "^2.26.0" + eslint-plugin-jsx-a11y "^6.5.1" + eslint-plugin-react "^7.31.7" + eslint-plugin-react-hooks "^4.5.0" -eslint-config-airbnb@^18.2.1: - version "18.2.1" - resolved "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz" - integrity sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg== - dependencies: - eslint-config-airbnb-base "^14.2.1" - object.assign "^4.1.2" - object.entries "^1.1.2" - -eslint-config-google@^0.14.0: - version "0.14.0" - resolved "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz" - integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== - -eslint-config-prettier@^8.3.0: - version "8.3.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== +eslint-config-prettier@^8.5.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207" + integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA== eslint-import-resolver-node@^0.3.6: version "0.3.6" @@ -3149,75 +2262,103 @@ eslint-import-resolver-node@^0.3.6: debug "^3.2.7" resolve "^1.20.0" -eslint-module-utils@^2.6.2: - version "2.6.2" - resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz" - integrity sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q== +eslint-import-resolver-node@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" + integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== dependencies: debug "^3.2.7" - pkg-dir "^2.0.0" + is-core-module "^2.11.0" + resolve "^1.22.1" -eslint-plugin-import@^2.23.3: - version "2.24.2" - resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz" - integrity sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q== +eslint-import-resolver-typescript@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.3.tgz#db5ed9e906651b7a59dd84870aaef0e78c663a05" + integrity sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ== dependencies: - array-includes "^3.1.3" - array.prototype.flat "^1.2.4" - debug "^2.6.9" + debug "^4.3.4" + enhanced-resolve "^5.10.0" + get-tsconfig "^4.2.0" + globby "^13.1.2" + is-core-module "^2.10.0" + is-glob "^4.0.3" + synckit "^0.8.4" + +eslint-module-utils@^2.7.4: + version "2.7.4" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" + integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.26.0: + version "2.27.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.4.tgz#319c2f6f6580e1678d674a258ee5e981c10cc25b" + integrity sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.0" + debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.6.2" - find-up "^2.0.0" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.7.4" has "^1.0.3" - is-core-module "^2.6.0" - minimatch "^3.0.4" - object.values "^1.1.4" - pkg-up "^2.0.0" - read-pkg-up "^3.0.0" - resolve "^1.20.0" - tsconfig-paths "^3.11.0" + is-core-module "^2.11.0" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.6" + resolve "^1.22.1" + semver "^6.3.0" + tsconfig-paths "^3.14.1" -eslint-plugin-jsx-a11y@^6.4.1: - version "6.4.1" - resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz" - integrity sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg== +eslint-plugin-jsx-a11y@^6.5.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976" + integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== dependencies: - "@babel/runtime" "^7.11.2" - aria-query "^4.2.2" - array-includes "^3.1.1" + "@babel/runtime" "^7.20.7" + aria-query "^5.1.3" + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" ast-types-flow "^0.0.7" - axe-core "^4.0.2" - axobject-query "^2.2.0" - damerau-levenshtein "^1.0.6" - emoji-regex "^9.0.0" + axe-core "^4.6.2" + axobject-query "^3.1.1" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" has "^1.0.3" - jsx-ast-utils "^3.1.0" - language-tags "^1.0.5" + jsx-ast-utils "^3.3.3" + language-tags "=1.0.5" + minimatch "^3.1.2" + object.entries "^1.1.6" + object.fromentries "^2.0.6" + semver "^6.3.0" -eslint-plugin-react-hooks@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz" - integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== +eslint-plugin-react-hooks@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== -eslint-plugin-react@^7.23.2: - version "7.25.1" - resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz" - integrity sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug== +eslint-plugin-react@^7.31.7: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.0.tgz#d80f794a638c5770f952ba2ae793f0a516be7c09" + integrity sha512-vSBi1+SrPiLZCGvxpiZIa28fMEUaMjXtCplrvxcIxGzmFiYdsXQDwInEjuv5/i/2CTTxbkS87tE8lsQ0Qxinbw== dependencies: - array-includes "^3.1.3" - array.prototype.flatmap "^1.2.4" + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" + array.prototype.tosorted "^1.1.1" doctrine "^2.1.0" - estraverse "^5.2.0" - has "^1.0.3" + estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.0.4" - object.entries "^1.1.4" - object.fromentries "^2.0.4" - object.values "^1.1.4" - prop-types "^15.7.2" - resolve "^2.0.0-next.3" - string.prototype.matchall "^4.0.5" + minimatch "^3.1.2" + object.entries "^1.1.6" + object.fromentries "^2.0.6" + object.hasown "^1.1.2" + object.values "^1.1.6" + prop-types "^15.8.1" + resolve "^2.0.0-next.4" + semver "^6.3.0" + string.prototype.matchall "^4.0.8" eslint-scope@^5.1.1: version "5.1.1" @@ -3227,12 +2368,13 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== dependencies: - eslint-visitor-keys "^1.1.0" + esrecurse "^4.3.0" + estraverse "^5.2.0" eslint-utils@^3.0.0: version "3.0.0" @@ -3241,81 +2383,75 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - eslint-visitor-keys@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint@^7.27.0: - version "7.32.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.28.0: + version "8.31.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.31.0.tgz#75028e77cbcff102a9feae1d718135931532d524" + integrity sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA== dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" + "@eslint/eslintrc" "^1.4.1" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" + regexpp "^3.2.0" + strip-ansi "^6.0.1" strip-json-comments "^3.1.0" - table "^6.0.9" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^9.4.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" + integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" esprima@^1.0.3: version "1.2.5" resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.5.tgz#0993502feaf668138325756f30f9a51feeec11e9" integrity sha1-CZNQL+r2aBODJXVvMPmlH+7sEek= -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - esprima@~1.0.2: version "1.0.4" resolved "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" @@ -3350,6 +2486,11 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + estraverse@~1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.3.2.tgz#37c2b893ef13d723f276d878d60d8535152a6c42" @@ -3360,16 +2501,6 @@ estraverse@~1.5.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz" integrity sha1-hno+jlip+EYYr7bC3bzZFrfLr3E= -estree-walker@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz" - integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== - -estree-walker@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" - integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== - esutils@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" @@ -3461,10 +2592,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.1.1: - version "3.2.7" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== +fast-glob@^3.2.11, fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -3472,7 +2603,7 @@ fast-glob@^3.1.1: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3549,12 +2680,13 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: - locate-path "^2.0.0" + locate-path "^6.0.0" + path-exists "^4.0.0" flat-cache@^3.0.4: version "3.0.4" @@ -3584,6 +2716,13 @@ follow-redirects@^1.14.0: resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz" integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + foreach@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" @@ -3612,15 +2751,6 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3631,10 +2761,20 @@ function-bind@^1.1.1: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== gauge@~2.7.3: version "2.7.4" @@ -3650,11 +2790,6 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" @@ -3664,6 +2799,15 @@ 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-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" @@ -3682,6 +2826,11 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-tsconfig@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.3.0.tgz#4c26fae115d1050e836aea65d6fe56b507ee249b" + integrity sha512-YCcF28IqSay3fqpIu5y3Krg/utCBHBeoflkZyHj/QcqI2nrLPC3ZegS9CmIo+hJb8K7aiGsuUl7PwWVjNG2HQQ== + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -3689,7 +2838,14 @@ glob-parent@^5.1.2: dependencies: is-glob "^4.0.1" -glob@^7.0.3, glob@^7.1.3: +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@7.1.7, glob@^7.1.3: version "7.1.7" resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== @@ -3718,46 +2874,70 @@ globals@^11.1.0: resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: - version "13.11.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz" - integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g== +globals@^13.19.0: + version "13.19.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" + integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== dependencies: type-fest "^0.20.2" -globby@^11.0.3: - version "11.0.4" - resolved "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globalyzer@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" + integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" slash "^3.0.0" -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= +globby@^13.1.2: + version "13.1.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.3.tgz#f62baf5720bcb2c1330c8d4ef222ee12318563ff" + integrity sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw== dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" + dir-glob "^3.0.1" + fast-glob "^3.2.11" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^4.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.8" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" graceful-fs@^4.2.4: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + gzip-size@^5.0.0: version "5.1.1" resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz" @@ -3771,6 +2951,11 @@ has-bigints@^1.0.1: resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== +has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" @@ -3781,11 +2966,28 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" @@ -3849,11 +3051,6 @@ hoopy@^0.1.4: resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz" integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - http-errors@1.7.2: version "1.7.2" resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz" @@ -3941,15 +3138,10 @@ ieee754@^1.2.1: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== immediate@~3.0.5: version "3.0.6" @@ -4001,6 +3193,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + side-channel "^1.0.4" + invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" @@ -4023,6 +3224,23 @@ is-any-array@^1.0.0: resolved "https://registry.yarnpkg.com/is-any-array/-/is-any-array-1.0.1.tgz#05fedec1a4dced1854bd279b2ec5df431e5bea9e" integrity sha512-m+FSiaONxBt2W0h9XOUngMBu/WW8uzAKbSk4Ty2aeCcQJ+muBqENexvxUHtDpX65fk5AMCROxqgNX0sSAHstcw== +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" @@ -4048,25 +3266,42 @@ is-buffer@^1.0.2, is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-callable@^1.1.3, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== -is-core-module@^2.2.0, is-core-module@^2.6.0: +is-core-module@^2.10.0, is-core-module@^2.11.0, is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-core-module@^2.2.0: version "2.6.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz" integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ== dependencies: has "^1.0.3" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== dependencies: has-tostringtag "^1.0.0" +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-electron@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-electron/-/is-electron-2.2.0.tgz" @@ -4101,19 +3336,19 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" -is-glob@^4.0.1: +is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" -is-module@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" - integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== -is-negative-zero@^2.0.1: +is-negative-zero@^2.0.1, is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== @@ -4135,24 +3370,10 @@ is-obj@^1.0.1: resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= -is-path-cwd@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^3.0.0: version "3.0.0" @@ -4172,11 +3393,23 @@ is-regexp@^1.0.0: resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + is-shared-array-buffer@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz" integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" @@ -4196,6 +3429,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.10, is-typed-array@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" @@ -4206,19 +3450,39 @@ is-url@^1.2.4: resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== -is-weakref@^1.0.1: +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakref@^1.0.1, is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== dependencies: call-bind "^1.0.2" +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -isarray@^2.0.1: +isarray@^2.0.1, isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== @@ -4233,14 +3497,6 @@ isexe@^2.0.0: resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -jest-worker@^24.9.0: - version "24.9.0" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz" - integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== - dependencies: - merge-stream "^2.0.0" - supports-color "^6.1.0" - jpeg-autorotate@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/jpeg-autorotate/-/jpeg-autorotate-7.1.1.tgz#c57905c6afd3b54373a6a1d0249ed6e07f7b043b" @@ -4257,34 +3513,28 @@ jpeg-js@^0.4.1, jpeg-js@^0.4.2: resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg== +js-sdsl@^4.1.4: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" + integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: - argparse "^1.0.7" - esprima "^4.0.0" + argparse "^2.0.1" jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" @@ -4295,11 +3545,6 @@ json-schema-traverse@^0.4.1: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" @@ -4312,21 +3557,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.2: - version "2.2.0" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0: +"jsx-ast-utils@^2.4.1 || ^3.0.0": version "3.2.0" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz" integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q== @@ -4334,6 +3565,14 @@ jsonfile@^4.0.0: array-includes "^3.1.2" object.assign "^4.1.2" +jsx-ast-utils@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" + integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== + dependencies: + array-includes "^3.1.5" + object.assign "^4.1.3" + jszip@3.7.1: version "3.7.1" resolved "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz" @@ -4361,10 +3600,10 @@ language-subtag-registry@~0.3.2: resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz" integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg== -language-tags@^1.0.5: +language-tags@=1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz" - integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" + integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== dependencies: language-subtag-registry "~0.3.2" @@ -4450,16 +3689,6 @@ listr2@^3.8.2: through "^2.3.8" wrap-ansi "^7.0.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - localforage@^1.8.1, localforage@^1.9.0: version "1.10.0" resolved "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz" @@ -4467,59 +3696,23 @@ localforage@^1.8.1, localforage@^1.9.0: dependencies: lie "3.1.1" -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" + p-locate "^5.0.0" lodash-es@^4.17.11, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.template@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= - lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" @@ -4572,13 +3765,6 @@ lru_map@^0.3.3: resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz" integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= -magic-string@^0.25.0, magic-string@^0.25.7: - version "0.25.7" - resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== - dependencies: - sourcemap-codec "^1.4.4" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz" @@ -4608,7 +3794,7 @@ merge-stream@^2.0.0: resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -4656,7 +3842,7 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -4673,6 +3859,11 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + mkdirp@^0.5.1, mkdirp@^0.5.5: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" @@ -4725,10 +3916,15 @@ ms@2.1.2, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -nanoid@^3.1.30: - version "3.3.1" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== natural-compare@^1.4.0: version "1.4.0" @@ -4771,36 +3967,37 @@ negotiator@0.6.2: resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -next-transpile-modules@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-9.0.0.tgz#133b1742af082e61cc76b02a0f12ffd40ce2bf90" - integrity sha512-VCNFOazIAnXn1hvgYYSTYMnoWgKgwlYh4lm1pKbSfiB3kj5ZYLcKVhfh3jkPOg1cnd9DP+pte9yCUocdPEUBTQ== +next-transpile-modules@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-10.0.0.tgz#7152880048835acb64d05fc7aa34910cbe7994da" + integrity sha512-FyeJ++Lm2Fq31gbThiRCrJlYpIY9QaI7A3TjuhQLzOix8ChQrvn5ny4MhfIthS5cy6+uK1AhDRvxVdW17y3Xdw== dependencies: - enhanced-resolve "^5.7.0" - escalade "^3.1.1" + enhanced-resolve "^5.10.0" -next@^12.1.0: - version "12.1.0" - resolved "https://registry.npmjs.org/next/-/next-12.1.0.tgz" - integrity sha512-s885kWvnIlxsUFHq9UGyIyLiuD0G3BUC/xrH0CEnH5lHEWkwQcHOORgbDF0hbrW9vr/7am4ETfX4A7M6DjrE7Q== +next@^13.0.6: + version "13.1.2" + resolved "https://registry.yarnpkg.com/next/-/next-13.1.2.tgz#4105b0cf238bb2f58d5e12dbded8cabb9785f2d9" + integrity sha512-Rdnnb2YH///w78FEOR/IQ6TXga+qpth4OqFSem48ng1PYYKr6XBsIk1XVaRcIGM3o6iiHnun0nJvkJHDf+ICyQ== dependencies: - "@next/env" "12.1.0" - caniuse-lite "^1.0.30001283" - postcss "8.4.5" - styled-jsx "5.0.0" - use-subscription "1.5.1" + "@next/env" "13.1.2" + "@swc/helpers" "0.4.14" + caniuse-lite "^1.0.30001406" + postcss "8.4.14" + styled-jsx "5.1.1" optionalDependencies: - "@next/swc-android-arm64" "12.1.0" - "@next/swc-darwin-arm64" "12.1.0" - "@next/swc-darwin-x64" "12.1.0" - "@next/swc-linux-arm-gnueabihf" "12.1.0" - "@next/swc-linux-arm64-gnu" "12.1.0" - "@next/swc-linux-arm64-musl" "12.1.0" - "@next/swc-linux-x64-gnu" "12.1.0" - "@next/swc-linux-x64-musl" "12.1.0" - "@next/swc-win32-arm64-msvc" "12.1.0" - "@next/swc-win32-ia32-msvc" "12.1.0" - "@next/swc-win32-x64-msvc" "12.1.0" + "@next/swc-android-arm-eabi" "13.1.2" + "@next/swc-android-arm64" "13.1.2" + "@next/swc-darwin-arm64" "13.1.2" + "@next/swc-darwin-x64" "13.1.2" + "@next/swc-freebsd-x64" "13.1.2" + "@next/swc-linux-arm-gnueabihf" "13.1.2" + "@next/swc-linux-arm64-gnu" "13.1.2" + "@next/swc-linux-arm64-musl" "13.1.2" + "@next/swc-linux-x64-gnu" "13.1.2" + "@next/swc-linux-x64-musl" "13.1.2" + "@next/swc-win32-arm64-msvc" "13.1.2" + "@next/swc-win32-ia32-msvc" "13.1.2" + "@next/swc-win32-x64-msvc" "13.1.2" node-fetch@^2.6.0: version "2.6.2" @@ -4814,21 +4011,6 @@ node-fetch@^2.6.1, node-fetch@~2.6.1: dependencies: whatwg-url "^5.0.0" -node-releases@^1.1.75: - version "1.1.75" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz" - integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw== - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" @@ -4856,7 +4038,7 @@ number-is-nan@^1.0.0: resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -4866,11 +4048,24 @@ object-inspect@^1.11.0, object-inspect@^1.9.0: resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== +object-inspect@^1.12.2: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + object-inspect@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-0.4.0.tgz#f5157c116c1455b243b06ee97703392c5ad89fec" integrity sha1-9RV8EWwUVbJDsG7pdwM5LFrYn+w= +object-is@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" @@ -4881,7 +4076,7 @@ object-keys@~0.4.0: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= -object.assign@^4.1.0, object.assign@^4.1.2: +object.assign@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -4891,33 +4086,50 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -object.entries@^1.1.2, object.entries@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz" - integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA== +object.assign@^4.1.3, object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" -object.fromentries@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz" - integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ== +object.entries@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23" + integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" - has "^1.0.3" + define-properties "^1.1.4" + es-abstract "^1.20.4" -object.values@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz" - integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== +object.fromentries@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" + integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.hasown@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" + integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== + dependencies: + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.values@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" on-finished@~2.3.0: version "2.3.0" @@ -4940,6 +4152,15 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + opencollective-postinstall@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" @@ -4962,24 +4183,19 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - p-try "^1.0.0" + yocto-queue "^0.1.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: - p-limit "^1.1.0" - -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + p-limit "^3.0.2" p-map@^4.0.0: version "4.0.0" @@ -5001,11 +4217,6 @@ p-timeout@^5.0.0: resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-5.0.2.tgz#d12964c4b2f988e15f72b455c2c428d82a0ec0a0" integrity sha512-sEmji9Yaq+Tw+STwsGAE56hf7gMy9p0tQfJojIAamB7WHJYJKf1qlsg9jqBWG8q9VCxKPhZaP/AcXwEoBcYQhQ== -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - pako@~1.0.2: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" @@ -5018,14 +4229,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-json@^5.0.0: version "5.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" @@ -5041,27 +4244,22 @@ parseurl@~1.3.3: resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -5071,13 +4269,6 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -5107,62 +4298,26 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -picomatch@^2.2.2, picomatch@^2.2.3: +picomatch@^2.2.3: version "2.3.0" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + piexifjs@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/piexifjs/-/piexifjs-1.0.6.tgz" integrity sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag== -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - pify@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz" - integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= - dependencies: - find-up "^2.1.0" - please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz" @@ -5180,14 +4335,14 @@ postcss-value-parser@^4.0.2: resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -postcss@8.4.5: - version "8.4.5" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz" - integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== +postcss@8.4.14: + version "8.4.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" + integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== dependencies: - nanoid "^3.1.30" + nanoid "^3.3.4" picocolors "^1.0.0" - source-map-js "^1.0.1" + source-map-js "^1.0.2" prelude-ls@^1.2.1: version "1.2.1" @@ -5199,17 +4354,12 @@ prettier@2.3.2: resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz" integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== -pretty-bytes@^5.3.0: - version "5.6.0" - resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" - integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -progress@^2.0.0, progress@^2.0.3: +progress@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -5281,7 +4431,7 @@ quote-stream@~0.0.0: minimist "0.0.8" through2 "~0.4.1" -randombytes@^2.0.1, randombytes@^2.1.0: +randombytes@^2.0.1: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -5353,14 +4503,13 @@ react-datepicker@^4.3.0: react-onclickoutside "^6.12.0" react-popper "^2.2.5" -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.23.0" react-dropzone@^11.2.4: version "11.3.4" @@ -5481,30 +4630,12 @@ react-window@^1.8.6: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@^17.0.2: - version "17.0.2" - resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz" - integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= - dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" - -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" @@ -5555,82 +4686,35 @@ readable-web-to-node-stream@^3.0.0: dependencies: readable-stream "^3.6.0" -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== - dependencies: - regenerate "^1.4.0" - -regenerate@^1.4.0: - version "1.4.2" - resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: version "0.13.9" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== - dependencies: - "@babel/runtime" "^7.8.4" - -regexp.prototype.flags@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz" - integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" + functions-have-names "^1.2.2" -regexpp@^3.1.0: +regexpp@^3.2.0: version "3.2.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpu-core@^4.7.1: - version "4.7.1" - resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz" - integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" - -regjsgen@^0.5.1: - version "0.5.2" - resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz" - integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== - -regjsparser@^0.6.4: - version "0.6.9" - resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz" - integrity sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ== - dependencies: - jsesc "~0.5.0" - repeat-string@^1.5.2: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -5646,7 +4730,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== -resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0: +resolve@^1.20.0: version "1.20.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -5654,13 +4738,23 @@ resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0: is-core-module "^2.2.0" path-parse "^1.0.6" -resolve@^2.0.0-next.3: - version "2.0.0-next.3" - resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz" - integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== +resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.4: + version "2.0.0-next.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" + integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" restore-cursor@^3.1.0: version "3.1.0" @@ -5687,13 +4781,6 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" @@ -5709,41 +4796,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-babel@^4.3.3: - version "4.4.0" - resolved "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-4.4.0.tgz" - integrity sha512-Lek/TYp1+7g7I+uMfJnnSJ7YWoD58ajo6Oarhlex7lvUce+RCKRuGRSgztDO3/MF/PuGKmUL5iTHKf208UNszw== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - rollup-pluginutils "^2.8.1" - -rollup-plugin-terser@^5.3.1: - version "5.3.1" - resolved "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.1.tgz" - integrity sha512-1pkwkervMJQGFYvM9nscrUoncPwiKR/K+bHdjv6PFgRo3cgPHoRT83y2Aa3GvINj4539S15t/tpFPb775TDs6w== - dependencies: - "@babel/code-frame" "^7.5.5" - jest-worker "^24.9.0" - rollup-pluginutils "^2.8.2" - serialize-javascript "^4.0.0" - terser "^4.6.2" - -rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2: - version "2.8.2" - resolved "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz" - integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== - dependencies: - estree-walker "^0.6.1" - -rollup@^1.31.1: - version "1.32.1" - resolved "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz" - integrity sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A== - dependencies: - "@types/estree" "*" - "@types/node" "*" - acorn "^7.1.0" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" @@ -5768,23 +4820,38 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.0, resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-filename@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" + integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== + dependencies: + truncate-utf8-bytes "^1.0.0" + sax@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" seedrandom@2.4.3: version "2.4.3" @@ -5796,25 +4863,15 @@ semver-compare@^1.0.0: resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz" integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= -"semver@2 || 3 || 4 || 5": - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: +semver@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver@^7.3.7: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: lru-cache "^6.0.0" @@ -5837,13 +4894,6 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - serve-static@1.14.1: version "1.14.1" resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz" @@ -5923,6 +4973,11 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + slice-ansi@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz" @@ -5941,30 +4996,12 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-js@^1.0.1: +source-map-js@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map-support@~0.5.12: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - -"source-map@>= 0.1.2", source-map@^0.7.3: +"source-map@>= 0.1.2": version "0.7.3" resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -5974,11 +5011,6 @@ source-map@^0.5.0, source-map@~0.5.1: resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - source-map@~0.1.33: version "0.1.43" resolved "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz" @@ -5986,42 +5018,6 @@ source-map@~0.1.33: dependencies: amdefine ">=0.0.4" -sourcemap-codec@^1.4.4: - version "1.4.8" - resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.10" - resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz" - integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - static-eval@~0.2.0: version "0.2.4" resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-0.2.4.tgz#b7d34d838937b969f9641ca07d48f8ede263ea7b" @@ -6051,6 +5047,13 @@ static-module@^1.0.0: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + string-argv@0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz" @@ -6082,18 +5085,18 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.matchall@^4.0.5: - version "4.0.5" - resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz" - integrity sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q== +string.prototype.matchall@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" + integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.2" - get-intrinsic "^1.1.1" - has-symbols "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" internal-slot "^1.0.3" - regexp.prototype.flags "^1.3.1" + regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" string.prototype.trimend@^1.0.4: @@ -6104,6 +5107,15 @@ string.prototype.trimend@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string.prototype.trimstart@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz" @@ -6112,6 +5124,15 @@ string.prototype.trimstart@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -6161,19 +5182,18 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= -strip-comments@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz" - integrity sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw== - dependencies: - babel-extract-comments "^1.0.0" - babel-plugin-transform-object-rest-spread "^6.26.0" - strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" @@ -6208,10 +5228,12 @@ styled-components@^5.3.5: shallowequal "^1.1.0" supports-color "^5.5.0" -styled-jsx@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.0.tgz" - integrity sha512-qUqsWoBquEdERe10EW8vLp3jT25s/ssG1/qX5gZ4wu15OZpmSMFI2v+fWlRhLfykA5rFtlJ1ME8A8pm/peV4WA== +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" stylis@^4.0.3: version "4.0.10" @@ -6225,13 +5247,6 @@ supports-color@^5.3.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" @@ -6239,51 +5254,29 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + synchronous-promise@^2.0.13: version "2.0.15" resolved "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.15.tgz" integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg== -table@^6.0.9: - version "6.7.1" - resolved "https://registry.npmjs.org/table/-/table-6.7.1.tgz" - integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== +synckit@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.4.tgz#0e6b392b73fafdafcde56692e3352500261d64ec" + integrity sha512-Dn2ZkzMdSX827QbowGbU/4yjWuvNaCoScLLoMo/yKbu+P4GBR6cRGKZH27k6a9bRzdqcyd1DE96pQtQ6uNkmyw== dependencies: - ajv "^8.0.1" - lodash.clonedeep "^4.5.0" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.0" - strip-ansi "^6.0.0" + "@pkgr/utils" "^2.3.1" + tslib "^2.4.0" tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -temp-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz" - integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= - -tempy@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/tempy/-/tempy-0.3.0.tgz" - integrity sha512-WrH/pui8YCwmeiAoxV+lpRH9HpRtgBhSR2ViBPgpGb/wnYDzp21R4MN45fsCGvLROvY67o3byhJRYRONJyImVQ== - dependencies: - temp-dir "^1.0.0" - type-fest "^0.3.1" - unique-string "^1.0.0" - -terser@^4.6.2: - version "4.8.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" - integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - tesseract.js-core@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tesseract.js-core/-/tesseract.js-core-2.2.0.tgz#6ef78051272a381969fac3e45a226e85022cffef" @@ -6324,6 +5317,14 @@ through@^2.3.8: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tiny-glob@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== + dependencies: + globalyzer "0.1.0" + globrex "^0.1.2" + tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" @@ -6369,19 +5370,26 @@ transformation-matrix@^2.10.0: resolved "https://registry.yarnpkg.com/transformation-matrix/-/transformation-matrix-2.10.0.tgz#a135638fdbf91da2e75316cf68648e2436c60a1d" integrity sha512-KH+vJ3eW9rZQMJY2nbhtX9ogUSb4vlolFaVJwe0J1nChBoE/G5+EVqwMgd8kxxL0pdb5PIeRypf3RjzgyDC+0A== +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ== + dependencies: + utf8-byte-length "^1.0.1" + tryer@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== -tsconfig-paths@^3.11.0: - version "3.11.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz" - integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA== +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" - minimist "^1.2.0" + minimist "^1.2.6" strip-bom "^3.0.0" tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: @@ -6394,6 +5402,11 @@ tslib@^2.0.3: resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + tsne-js@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tsne-js/-/tsne-js-1.0.3.tgz#9dca61a6edabf0d0b62ea8490890c31fc7a735f7" @@ -6429,11 +5442,6 @@ type-fest@^0.21.3: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-fest@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz" - integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== - type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" @@ -6442,6 +5450,15 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -6477,6 +5494,16 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + uncontrollable@^7.2.1: version "7.2.1" resolved "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz" @@ -6487,56 +5514,16 @@ uncontrollable@^7.2.1: invariant "^2.2.4" react-lifecycles-compat "^3.0.4" -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== - -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== - dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" - -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== - -unicode-property-aliases-ecmascript@^1.0.4: - version "1.1.0" - resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz" - integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== - uniq@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= -unique-string@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz" - integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= - dependencies: - crypto-random-string "^1.0.0" - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -upath@^1.1.2, upath@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" @@ -6544,12 +5531,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-subscription@1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz" - integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== - dependencies: - object-assign "^4.1.1" +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" + integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA== util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" @@ -6566,19 +5551,6 @@ uuid@^8.3.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - vary@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" @@ -6622,14 +5594,6 @@ webpack-bundle-analyzer@3.6.1: opener "^1.5.1" ws "^6.0.0" -webpack-sources@^1.3.0: - version "1.4.3" - resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -6649,6 +5613,28 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" @@ -6678,62 +5664,6 @@ wordwrap@0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= -workbox-background-sync@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz" - integrity sha512-AH6x5pYq4vwQvfRDWH+vfOePfPIYQ00nCEB7dJRU1e0n9+9HMRyvI63FlDvtFT2AvXVRsXvUt7DNMEToyJLpSA== - dependencies: - workbox-core "^5.1.4" - -workbox-broadcast-update@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-5.1.4.tgz" - integrity sha512-HTyTWkqXvHRuqY73XrwvXPud/FN6x3ROzkfFPsRjtw/kGZuZkPzfeH531qdUGfhtwjmtO/ZzXcWErqVzJNdXaA== - dependencies: - workbox-core "^5.1.4" - -workbox-build@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-build/-/workbox-build-5.1.4.tgz" - integrity sha512-xUcZn6SYU8usjOlfLb9Y2/f86Gdo+fy1fXgH8tJHjxgpo53VVsqRX0lUDw8/JuyzNmXuo8vXX14pXX2oIm9Bow== - dependencies: - "@babel/core" "^7.8.4" - "@babel/preset-env" "^7.8.4" - "@babel/runtime" "^7.8.4" - "@hapi/joi" "^15.1.0" - "@rollup/plugin-node-resolve" "^7.1.1" - "@rollup/plugin-replace" "^2.3.1" - "@surma/rollup-plugin-off-main-thread" "^1.1.1" - common-tags "^1.8.0" - fast-json-stable-stringify "^2.1.0" - fs-extra "^8.1.0" - glob "^7.1.6" - lodash.template "^4.5.0" - pretty-bytes "^5.3.0" - rollup "^1.31.1" - rollup-plugin-babel "^4.3.3" - rollup-plugin-terser "^5.3.1" - source-map "^0.7.3" - source-map-url "^0.4.0" - stringify-object "^3.3.0" - strip-comments "^1.0.2" - tempy "^0.3.0" - upath "^1.2.0" - workbox-background-sync "^5.1.4" - workbox-broadcast-update "^5.1.4" - workbox-cacheable-response "^5.1.4" - workbox-core "^5.1.4" - workbox-expiration "^5.1.4" - workbox-google-analytics "^5.1.4" - workbox-navigation-preload "^5.1.4" - workbox-precaching "^5.1.4" - workbox-range-requests "^5.1.4" - workbox-routing "^5.1.4" - workbox-strategies "^5.1.4" - workbox-streams "^5.1.4" - workbox-sw "^5.1.4" - workbox-window "^5.1.4" - workbox-cacheable-response@6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.3.0.tgz" @@ -6741,23 +5671,11 @@ workbox-cacheable-response@6.3.0: dependencies: workbox-core "6.3.0" -workbox-cacheable-response@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-5.1.4.tgz" - integrity sha512-0bfvMZs0Of1S5cdswfQK0BXt6ulU5kVD4lwer2CeI+03czHprXR3V4Y8lPTooamn7eHP8Iywi5QjyAMjw0qauA== - dependencies: - workbox-core "^5.1.4" - workbox-core@6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/workbox-core/-/workbox-core-6.3.0.tgz" integrity sha512-SufToEV3SOLwwz3j+P4pgkfpzLRUlR17sX3p/LrMHP/brYKvJQqjTwtSvaCkkAX0RPHX2TFHmN8xhPP1bpmomg== -workbox-core@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz" - integrity sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg== - workbox-expiration@6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.3.0.tgz" @@ -6766,30 +5684,6 @@ workbox-expiration@6.3.0: idb "^6.0.0" workbox-core "6.3.0" -workbox-expiration@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-5.1.4.tgz" - integrity sha512-oDO/5iC65h2Eq7jctAv858W2+CeRW5e0jZBMNRXpzp0ZPvuT6GblUiHnAsC5W5lANs1QS9atVOm4ifrBiYY7AQ== - dependencies: - workbox-core "^5.1.4" - -workbox-google-analytics@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-5.1.4.tgz" - integrity sha512-0IFhKoEVrreHpKgcOoddV+oIaVXBFKXUzJVBI+nb0bxmcwYuZMdteBTp8AEDJacENtc9xbR0wa9RDCnYsCDLjA== - dependencies: - workbox-background-sync "^5.1.4" - workbox-core "^5.1.4" - workbox-routing "^5.1.4" - workbox-strategies "^5.1.4" - -workbox-navigation-preload@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-5.1.4.tgz" - integrity sha512-Wf03osvK0wTflAfKXba//QmWC5BIaIZARU03JIhAEO2wSB2BDROWI8Q/zmianf54kdV7e1eLaIEZhth4K4MyfQ== - dependencies: - workbox-core "^5.1.4" - workbox-precaching@6.3.0, workbox-precaching@^6.1.5: version "6.3.0" resolved "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.3.0.tgz" @@ -6799,20 +5693,6 @@ workbox-precaching@6.3.0, workbox-precaching@^6.1.5: workbox-routing "6.3.0" workbox-strategies "6.3.0" -workbox-precaching@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-5.1.4.tgz" - integrity sha512-gCIFrBXmVQLFwvAzuGLCmkUYGVhBb7D1k/IL7pUJUO5xacjLcFUaLnnsoVepBGAiKw34HU1y/YuqvTKim9qAZA== - dependencies: - workbox-core "^5.1.4" - -workbox-range-requests@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-5.1.4.tgz" - integrity sha512-1HSujLjgTeoxHrMR2muDW2dKdxqCGMc1KbeyGcmjZZAizJTFwu7CWLDmLv6O1ceWYrhfuLFJO+umYMddk2XMhw== - dependencies: - workbox-core "^5.1.4" - workbox-recipes@^6.1.5: version "6.3.0" resolved "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.3.0.tgz" @@ -6832,13 +5712,6 @@ workbox-routing@6.3.0, workbox-routing@^6.1.5: dependencies: workbox-core "6.3.0" -workbox-routing@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-routing/-/workbox-routing-5.1.4.tgz" - integrity sha512-8ljknRfqE1vEQtnMtzfksL+UXO822jJlHTIR7+BtJuxQ17+WPZfsHqvk1ynR/v0EHik4x2+826Hkwpgh4GKDCw== - dependencies: - workbox-core "^5.1.4" - workbox-strategies@6.3.0, workbox-strategies@^6.1.5: version "6.3.0" resolved "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.3.0.tgz" @@ -6846,46 +5719,6 @@ workbox-strategies@6.3.0, workbox-strategies@^6.1.5: dependencies: workbox-core "6.3.0" -workbox-strategies@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-5.1.4.tgz" - integrity sha512-VVS57LpaJTdjW3RgZvPwX0NlhNmscR7OQ9bP+N/34cYMDzXLyA6kqWffP6QKXSkca1OFo/v6v7hW7zrrguo6EA== - dependencies: - workbox-core "^5.1.4" - workbox-routing "^5.1.4" - -workbox-streams@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-streams/-/workbox-streams-5.1.4.tgz" - integrity sha512-xU8yuF1hI/XcVhJUAfbQLa1guQUhdLMPQJkdT0kn6HP5CwiPOGiXnSFq80rAG4b1kJUChQQIGPrq439FQUNVrw== - dependencies: - workbox-core "^5.1.4" - workbox-routing "^5.1.4" - -workbox-sw@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-sw/-/workbox-sw-5.1.4.tgz" - integrity sha512-9xKnKw95aXwSNc8kk8gki4HU0g0W6KXu+xks7wFuC7h0sembFnTrKtckqZxbSod41TDaGh+gWUA5IRXrL0ECRA== - -workbox-webpack-plugin@^5.1.4: - version "5.1.4" - resolved "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-5.1.4.tgz" - integrity sha512-PZafF4HpugZndqISi3rZ4ZK4A4DxO8rAqt2FwRptgsDx7NF8TVKP86/huHquUsRjMGQllsNdn4FNl8CD/UvKmQ== - dependencies: - "@babel/runtime" "^7.5.5" - fast-json-stable-stringify "^2.0.0" - source-map-url "^0.4.0" - upath "^1.1.2" - webpack-sources "^1.3.0" - workbox-build "^5.1.4" - -workbox-window@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-5.1.4.tgz#2740f7dea7f93b99326179a62f1cc0ca2c93c863" - integrity sha512-vXQtgTeMCUq/4pBWMfQX8Ee7N2wVC4Q7XYFqLnfbXJ2hqew/cU1uMTD2KqGEgEpE4/30luxIxgE+LkIa8glBYw== - dependencies: - workbox-core "^5.1.4" - workbox-window@^6.1.5: version "6.3.0" resolved "https://registry.npmjs.org/workbox-window/-/workbox-window-6.3.0.tgz" @@ -6963,6 +5796,11 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + yup@^0.29.3: version "0.29.3" resolved "https://registry.npmjs.org/yup/-/yup-0.29.3.tgz"