diff --git a/.eslintignore b/.eslintignore index dc7fe3106..256b5ff45 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,4 +5,3 @@ /tmp/** /coverage/** /custom/** -!.eslintrc.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 29a8d474a..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,304 +0,0 @@ -module.exports = { - root: true, - - extends: [ - 'eslint:recommended', - 'plugin:import/typescript', - 'plugin:compat/recommended', - 'plugin:tailwindcss/recommended', - ], - - env: { - browser: true, - node: true, - es6: true, - jest: true, - }, - - globals: { - ATTACHMENT_HOST: false, - }, - - plugins: [ - 'jsdoc', - 'react', - 'jsx-a11y', - 'import', - 'promise', - 'react-hooks', - '@typescript-eslint', - ], - - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - experimentalObjectRestSpread: true, - jsx: true, - }, - ecmaVersion: 2018, - }, - - settings: { - react: { - version: 'detect', - }, - 'import/extensions': ['.js', '.jsx', '.cjs', '.mjs', '.ts', '.tsx'], - 'import/ignore': [ - 'node_modules', - '\\.(css|scss|json)$', - ], - 'import/resolver': { - typescript: true, - node: true, - }, - polyfills: [ - 'es:all', // core-js - 'fetch', // not polyfilled, but ignore it - 'IntersectionObserver', // npm:intersection-observer - 'Promise', // core-js - 'ResizeObserver', // npm:resize-observer-polyfill - 'URL', // core-js - 'URLSearchParams', // core-js - ], - tailwindcss: { - config: 'tailwind.config.cjs', - }, - }, - - rules: { - 'brace-style': 'error', - 'comma-dangle': ['error', 'always-multiline'], - 'comma-spacing': [ - 'warn', - { - before: false, - after: true, - }, - ], - 'comma-style': ['warn', 'last'], - 'import/no-duplicates': 'error', - 'space-before-function-paren': ['error', 'never'], - 'space-infix-ops': 'error', - 'space-in-parens': ['error', 'never'], - 'keyword-spacing': 'error', - 'dot-notation': 'error', - eqeqeq: 'error', - indent: ['error', 2, { - SwitchCase: 1, // https://stackoverflow.com/a/53055584/8811886 - ignoredNodes: ['TemplateLiteral'], - }], - 'jsx-quotes': ['error', 'prefer-single'], - 'key-spacing': [ - 'error', - { mode: 'minimum' }, - ], - 'no-catch-shadow': 'error', - 'no-cond-assign': 'error', - 'no-console': [ - 'warn', - { - allow: [ - 'error', - 'warn', - ], - }, - ], - 'no-extra-semi': 'error', - 'no-const-assign': 'error', - 'no-fallthrough': 'error', - 'no-irregular-whitespace': 'error', - 'no-loop-func': 'error', - 'no-mixed-spaces-and-tabs': 'error', - 'no-nested-ternary': 'warn', - 'no-restricted-imports': ['error', { - patterns: [{ - group: ['react-inlinesvg'], - message: 'Use the SvgIcon component instead.', - }], - }], - 'no-trailing-spaces': 'warn', - 'no-undef': 'error', - 'no-unreachable': 'error', - 'no-unused-expressions': 'error', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - vars: 'all', - args: 'none', - ignoreRestSiblings: true, - }, - ], - 'no-useless-escape': 'warn', - 'no-var': 'error', - 'object-curly-spacing': ['error', 'always'], - 'padded-blocks': [ - 'error', - { - classes: 'always', - }, - ], - 'prefer-const': 'error', - quotes: ['error', 'single'], - semi: 'error', - 'space-unary-ops': [ - 'error', - { - words: true, - nonwords: false, - }, - ], - strict: 'off', - 'valid-typeof': 'error', - - 'react/jsx-boolean-value': 'error', - 'react/jsx-closing-bracket-location': ['error', 'line-aligned'], - 'react/jsx-curly-spacing': 'error', - 'react/jsx-equals-spacing': 'error', - 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], - 'react/jsx-indent': ['error', 2], - // 'react/jsx-no-bind': ['error'], - 'react/jsx-no-comment-textnodes': 'error', - 'react/jsx-no-duplicate-props': 'error', - 'react/jsx-no-undef': 'error', - 'react/jsx-tag-spacing': 'error', - 'react/jsx-uses-react': 'error', - 'react/jsx-uses-vars': 'error', - 'react/jsx-wrap-multilines': 'error', - 'react/no-multi-comp': 'off', - 'react/no-string-refs': 'error', - 'react/self-closing-comp': 'error', - - 'jsx-a11y/accessible-emoji': 'warn', - 'jsx-a11y/alt-text': 'warn', - 'jsx-a11y/anchor-has-content': 'warn', - 'jsx-a11y/anchor-is-valid': [ - 'warn', - { - components: [ - 'Link', - 'NavLink', - ], - specialLink: [ - 'to', - ], - aspect: [ - 'noHref', - 'invalidHref', - 'preferButton', - ], - }, - ], - 'jsx-a11y/aria-activedescendant-has-tabindex': 'warn', - 'jsx-a11y/aria-props': 'warn', - 'jsx-a11y/aria-proptypes': 'warn', - 'jsx-a11y/aria-role': 'warn', - 'jsx-a11y/aria-unsupported-elements': 'warn', - 'jsx-a11y/heading-has-content': 'warn', - 'jsx-a11y/html-has-lang': 'warn', - 'jsx-a11y/iframe-has-title': 'warn', - 'jsx-a11y/img-redundant-alt': 'warn', - 'jsx-a11y/interactive-supports-focus': 'warn', - 'jsx-a11y/label-has-for': 'off', - 'jsx-a11y/mouse-events-have-key-events': 'warn', - 'jsx-a11y/no-access-key': 'warn', - 'jsx-a11y/no-distracting-elements': 'warn', - 'jsx-a11y/no-noninteractive-element-interactions': [ - 'warn', - { - handlers: [ - 'onClick', - ], - }, - ], - 'jsx-a11y/no-onchange': 'warn', - 'jsx-a11y/no-redundant-roles': 'warn', - 'jsx-a11y/no-static-element-interactions': [ - 'warn', - { - handlers: [ - 'onClick', - ], - }, - ], - 'jsx-a11y/role-has-required-aria-props': 'warn', - 'jsx-a11y/role-supports-aria-props': 'off', - 'jsx-a11y/scope': 'warn', - 'jsx-a11y/tabindex-no-positive': 'warn', - - 'import/extensions': [ - 'error', - 'always', - { - js: 'never', - mjs: 'ignorePackages', - jsx: 'never', - ts: 'never', - tsx: 'never', - }, - ], - 'import/newline-after-import': 'error', - 'import/no-extraneous-dependencies': 'error', - 'import/no-unresolved': 'error', - 'import/no-webpack-loader-syntax': 'error', - 'import/order': [ - 'error', - { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - 'object', - 'type', - ], - 'newlines-between': 'always', - alphabetize: { order: 'asc' }, - }, - ], - '@typescript-eslint/member-delimiter-style': 'error', - - 'promise/catch-or-return': 'error', - - 'react-hooks/rules-of-hooks': 'error', - - 'tailwindcss/classnames-order': [ - 'error', - { - classRegex: '^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$', - config: 'tailwind.config.cjs', - }, - ], - 'tailwindcss/migration-from-tailwind-2': 'error', - }, - overrides: [ - { - files: ['**/*.ts', '**/*.tsx'], - rules: { - 'no-undef': 'off', // https://stackoverflow.com/a/69155899 - 'space-before-function-paren': 'off', - }, - parser: '@typescript-eslint/parser', - }, - { - // Only enforce JSDoc comments on UI components for now. - // https://www.npmjs.com/package/eslint-plugin-jsdoc - files: ['src/components/ui/**/*'], - rules: { - 'jsdoc/require-jsdoc': ['error', { - publicOnly: true, - require: { - ArrowFunctionExpression: true, - ClassDeclaration: true, - ClassExpression: true, - FunctionDeclaration: true, - FunctionExpression: true, - MethodDefinition: true, - }, - }], - }, - }, - ], -}; diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..209da449d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,350 @@ +{ + "root": true, + "extends": [ + "eslint:recommended", + "plugin:import/typescript", + "plugin:compat/recommended", + "plugin:tailwindcss/recommended" + ], + "env": { + "browser": true, + "node": true, + "es6": true, + "jest": true + }, + "globals": { + "ATTACHMENT_HOST": false + }, + "plugins": [ + "jsdoc", + "react", + "jsx-a11y", + "import", + "promise", + "react-hooks", + "@typescript-eslint" + ], + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "ecmaVersion": 2018 + }, + "settings": { + "react": { + "version": "detect" + }, + "import/extensions": [ + ".js", + ".jsx", + ".cjs", + ".mjs", + ".ts", + ".tsx" + ], + "import/ignore": [ + "node_modules", + "\\.(css|scss|json)$" + ], + "import/resolver": { + "typescript": true, + "node": true + }, + "polyfills": [ + "es:all", + "fetch", + "IntersectionObserver", + "Promise", + "ResizeObserver", + "URL", + "URLSearchParams" + ], + "tailwindcss": { + "config": "tailwind.config.ts" + } + }, + "rules": { + "brace-style": "error", + "comma-dangle": [ + "error", + "always-multiline" + ], + "comma-spacing": [ + "warn", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "warn", + "last" + ], + "import/no-duplicates": "error", + "space-before-function-paren": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-in-parens": [ + "error", + "never" + ], + "keyword-spacing": "error", + "dot-notation": "error", + "eqeqeq": "error", + "indent": [ + "error", + 2, + { + "SwitchCase": 1, + "ignoredNodes": [ + "TemplateLiteral" + ] + } + ], + "jsx-quotes": [ + "error", + "prefer-single" + ], + "key-spacing": [ + "error", + { + "mode": "minimum" + } + ], + "no-catch-shadow": "error", + "no-cond-assign": "error", + "no-console": [ + "warn", + { + "allow": [ + "error", + "warn" + ] + } + ], + "no-extra-semi": "error", + "no-const-assign": "error", + "no-fallthrough": "error", + "no-irregular-whitespace": "error", + "no-loop-func": "error", + "no-mixed-spaces-and-tabs": "error", + "no-nested-ternary": "warn", + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "react-inlinesvg" + ], + "message": "Use the SvgIcon component instead." + } + ] + } + ], + "no-trailing-spaces": "warn", + "no-undef": "error", + "no-unreachable": "error", + "no-unused-expressions": "error", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "vars": "all", + "args": "none", + "ignoreRestSiblings": true + } + ], + "no-useless-escape": "warn", + "no-var": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "always" + } + ], + "prefer-const": "error", + "quotes": [ + "error", + "single" + ], + "semi": "error", + "space-unary-ops": [ + "error", + { + "words": true, + "nonwords": false + } + ], + "strict": "off", + "valid-typeof": "error", + "react/jsx-boolean-value": "error", + "react/jsx-closing-bracket-location": [ + "error", + "line-aligned" + ], + "react/jsx-curly-spacing": "error", + "react/jsx-equals-spacing": "error", + "react/jsx-first-prop-new-line": [ + "error", + "multiline-multiprop" + ], + "react/jsx-indent": [ + "error", + 2 + ], + "react/jsx-no-comment-textnodes": "error", + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-undef": "error", + "react/jsx-tag-spacing": "error", + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "react/jsx-wrap-multilines": "error", + "react/no-multi-comp": "off", + "react/no-string-refs": "error", + "react/self-closing-comp": "error", + "jsx-a11y/accessible-emoji": "warn", + "jsx-a11y/alt-text": "warn", + "jsx-a11y/anchor-has-content": "warn", + "jsx-a11y/anchor-is-valid": [ + "warn", + { + "components": [ + "Link", + "NavLink" + ], + "specialLink": [ + "to" + ], + "aspect": [ + "noHref", + "invalidHref", + "preferButton" + ] + } + ], + "jsx-a11y/aria-activedescendant-has-tabindex": "warn", + "jsx-a11y/aria-props": "warn", + "jsx-a11y/aria-proptypes": "warn", + "jsx-a11y/aria-role": "warn", + "jsx-a11y/aria-unsupported-elements": "warn", + "jsx-a11y/heading-has-content": "warn", + "jsx-a11y/html-has-lang": "warn", + "jsx-a11y/iframe-has-title": "warn", + "jsx-a11y/img-redundant-alt": "warn", + "jsx-a11y/interactive-supports-focus": "warn", + "jsx-a11y/label-has-for": "off", + "jsx-a11y/mouse-events-have-key-events": "warn", + "jsx-a11y/no-access-key": "warn", + "jsx-a11y/no-distracting-elements": "warn", + "jsx-a11y/no-noninteractive-element-interactions": [ + "warn", + { + "handlers": [ + "onClick" + ] + } + ], + "jsx-a11y/no-onchange": "warn", + "jsx-a11y/no-redundant-roles": "warn", + "jsx-a11y/no-static-element-interactions": [ + "warn", + { + "handlers": [ + "onClick" + ] + } + ], + "jsx-a11y/role-has-required-aria-props": "warn", + "jsx-a11y/role-supports-aria-props": "off", + "jsx-a11y/scope": "warn", + "jsx-a11y/tabindex-no-positive": "warn", + "import/extensions": [ + "error", + "always", + { + "js": "never", + "mjs": "ignorePackages", + "jsx": "never", + "ts": "never", + "tsx": "never" + } + ], + "import/newline-after-import": "error", + "import/no-extraneous-dependencies": "error", + "import/no-unresolved": "error", + "import/no-webpack-loader-syntax": "error", + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + "object", + "type" + ], + "newlines-between": "always", + "alphabetize": { + "order": "asc" + } + } + ], + "@typescript-eslint/member-delimiter-style": "error", + "promise/catch-or-return": "error", + "react-hooks/rules-of-hooks": "error", + "tailwindcss/classnames-order": [ + "error", + { + "classRegex": "^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$", + "config": "tailwind.config.ts" + } + ], + "tailwindcss/migration-from-tailwind-2": "error" + }, + "overrides": [ + { + "files": [ + "**/*.ts", + "**/*.tsx" + ], + "rules": { + "no-undef": "off", + "space-before-function-paren": "off" + }, + "parser": "@typescript-eslint/parser" + }, + { + "files": [ + "src/components/ui/**/*" + ], + "rules": { + "jsdoc/require-jsdoc": [ + "error", + { + "publicOnly": true, + "require": { + "ArrowFunctionExpression": true, + "ClassDeclaration": true, + "ClassExpression": true, + "FunctionDeclaration": true, + "FunctionExpression": true, + "MethodDefinition": true + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dbc2c6b3a..30ce26ee6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: node:20 +image: node:21 variables: NODE_ENV: test @@ -45,7 +45,7 @@ lint: - "**/*.scss" - "**/*.css" - ".eslintignore" - - ".eslintrc.cjs" + - ".eslintrc.json" - ".stylelintrc.json" build: @@ -111,9 +111,9 @@ pages: docker: stage: deploy - image: docker:24.0.6 + image: docker:24.0.7 services: - - docker:24.0.6-dind + - docker:24.0.7-dind tags: - dind # https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 000000000..f2bf4259f --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,11 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) +# and commit this file to your remote git repository to share the goodness with others. + +# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart + +tasks: + - init: yarn install && yarn run build + command: yarn run start + + diff --git a/.stylelintrc.json b/.stylelintrc.json index d01dd67b6..f2e80958d 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -7,10 +7,8 @@ "color-function-notation": null, "custom-property-pattern": null, "declaration-block-no-redundant-longhand-properties": null, - "declaration-colon-newline-after": null, "declaration-empty-line-before": "never", "font-family-no-missing-generic-family-keyword": [true, { "ignoreFontFamilies": ["ForkAwesome", "Font Awesome 5 Free"] }], - "max-line-length": null, "no-descending-specificity": null, "no-duplicate-selectors": null, "no-invalid-position-at-import-rule": null, diff --git a/.tool-versions b/.tool-versions index 6de89a83a..b5ed8dc79 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 20.0.0 +nodejs 21.4.0 diff --git a/Dockerfile b/Dockerfile index 2765f2053..fa790e7ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20 as build +FROM node:21 as build WORKDIR /app COPY package.json . COPY yarn.lock . diff --git a/Dockerfile.dev b/Dockerfile.dev index 1e6056945..bb15e6d86 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:20 +FROM node:21 RUN apt-get update &&\ apt-get install -y inotify-tools &&\ diff --git a/package.json b/package.json index d90340b7f..ba82523fb 100644 --- a/package.json +++ b/package.json @@ -47,28 +47,30 @@ "@emoji-mart/data": "^1.1.2", "@floating-ui/react": "^0.26.0", "@fontsource/inter": "^5.0.0", + "@fontsource/noto-sans-javanese": "^5.0.16", "@fontsource/roboto-mono": "^5.0.0", "@fontsource/tajawal": "^5.0.8", "@gamestdio/websocket": "^0.3.2", - "@lexical/clipboard": "^0.12.2", - "@lexical/hashtag": "^0.12.2", - "@lexical/link": "^0.12.2", - "@lexical/react": "^0.12.2", - "@lexical/selection": "^0.12.2", - "@lexical/utils": "^0.12.2", + "@lexical/clipboard": "^0.12.4", + "@lexical/hashtag": "^0.12.4", + "@lexical/link": "^0.12.4", + "@lexical/react": "^0.12.4", + "@lexical/selection": "^0.12.4", + "@lexical/utils": "^0.12.4", "@popperjs/core": "^2.11.5", "@reach/combobox": "^0.18.0", "@reach/menu-button": "^0.18.0", "@reach/popover": "^0.18.0", "@reach/rect": "^0.18.0", "@reach/tabs": "^0.18.0", - "@reduxjs/toolkit": "^1.8.1", + "@reduxjs/toolkit": "^2.0.1", "@sentry/browser": "^7.74.1", "@sentry/react": "^7.74.1", + "@soapbox.pub/wasmboy": "^0.8.0", "@tabler/icons": "^2.0.0", "@tailwindcss/aspect-ratio": "^0.4.2", - "@tailwindcss/forms": "^0.5.3", - "@tailwindcss/typography": "^0.5.9", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", "@tanstack/react-query": "^5.0.0", "@types/escape-html": "^1.0.1", "@types/http-link-header": "^1.0.3", @@ -81,11 +83,11 @@ "@types/react-datepicker": "^4.4.2", "@types/react-dom": "^18.0.10", "@types/react-helmet": "^6.1.5", - "@types/react-motion": "^0.0.36", + "@types/react-motion": "^0.0.40", "@types/react-router-dom": "^5.3.3", "@types/react-sparklines": "^1.7.2", "@types/react-swipeable-views": "^0.13.1", - "@types/redux-mock-store": "^1.0.3", + "@types/redux-mock-store": "^1.0.6", "@types/semver": "^7.3.9", "@types/uuid": "^9.0.0", "@vitejs/plugin-react": "^4.0.4", @@ -99,6 +101,7 @@ "bowser": "^2.11.0", "browserslist": "^4.16.6", "clsx": "^2.0.0", + "comlink": "^4.4.1", "core-js": "^3.27.2", "cryptocurrency-icons": "^0.18.1", "cssnano": "^6.0.0", @@ -109,15 +112,15 @@ "escape-html": "^1.0.3", "exifr": "^7.1.3", "graphemesplit": "^2.4.4", - "html-react-parser": "^4.2.2", + "html-react-parser": "^5.0.0", "http-link-header": "^1.0.2", "immer": "^10.0.0", "immutable": "^4.2.1", "intersection-observer": "^0.12.2", - "intl-messageformat": "10.5.4", + "intl-messageformat": "10.5.8", "intl-pluralrules": "^2.0.0", "leaflet": "^1.8.0", - "lexical": "^0.12.2", + "lexical": "^0.12.4", "line-awesome": "^1.3.0", "localforage": "^1.10.0", "lodash": "^4.7.11", @@ -143,7 +146,7 @@ "react-motion": "^0.5.2", "react-overlays": "^0.9.0", "react-popper": "^2.3.0", - "react-redux": "^8.0.0", + "react-redux": "^9.0.4", "react-router-dom": "^5.3.0", "react-router-dom-v5-compat": "^6.6.2", "react-router-scroll-4": "^1.0.0-beta.2", @@ -152,12 +155,12 @@ "react-sticky-box": "^2.0.0", "react-swipeable-views": "^0.14.0", "react-virtuoso": "^4.3.11", - "redux": "^4.1.1", + "redux": "^5.0.0", "redux-immutable": "^4.0.0", - "redux-thunk": "^2.2.0", - "reselect": "^4.0.0", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.0", "resize-observer-polyfill": "^1.5.1", - "sass": "^1.66.1", + "sass": "^1.69.5", "semver": "^7.3.8", "stringz": "^2.0.0", "substring-trie": "^1.0.2", @@ -168,11 +171,11 @@ "typescript": "^5.1.3", "util": "^0.12.4", "uuid": "^9.0.0", - "vite": "^4.4.9", + "vite": "^5.0.10", "vite-plugin-compile-time": "^0.2.1", "vite-plugin-html": "^3.2.0", "vite-plugin-require": "^1.1.10", - "vite-plugin-static-copy": "^0.17.0", + "vite-plugin-static-copy": "^1.0.0", "wicg-inert": "^3.1.1", "zod": "^3.21.4" }, @@ -200,17 +203,17 @@ "eslint-plugin-tailwindcss": "^3.13.0", "fake-indexeddb": "^5.0.0", "husky": "^8.0.0", - "jsdom": "^22.1.0", + "jsdom": "^23.0.0", "lint-staged": ">=10", "react-intl-translations-manager": "^5.0.3", "react-refresh": "^0.14.0", "rollup-plugin-visualizer": "^5.9.2", - "stylelint": "^15.10.3", - "stylelint-config-standard-scss": "^11.0.0", - "tailwindcss": "^3.3.3", + "stylelint": "^16.0.2", + "stylelint-config-standard-scss": "^12.0.0", + "tailwindcss": "^3.4.0", "vite-plugin-checker": "^0.6.2", - "vite-plugin-pwa": "^0.16.5", - "vitest": "^0.34.4" + "vite-plugin-pwa": "^0.17.0", + "vitest": "^1.0.0" }, "resolutions": { "@types/react": "^18.0.26", diff --git a/postcss.config.cjs b/postcss.config.cjs index ddc63e8c8..4fd759756 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,7 +1,10 @@ -module.exports = ({ env }) => ({ +/** @type {import('postcss-load-config').ConfigFn} */ +const config = ({ env }) => ({ plugins: { tailwindcss: {}, autoprefixer: {}, cssnano: env === 'production' ? {} : false, }, }); + +module.exports = config; \ No newline at end of file diff --git a/src/actions/emoji-reacts.ts b/src/actions/emoji-reacts.ts index 764ed0139..1e8991824 100644 --- a/src/actions/emoji-reacts.ts +++ b/src/actions/emoji-reacts.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { List as ImmutableList } from 'immutable'; import { isLoggedIn } from 'soapbox/utils/auth'; @@ -8,7 +8,7 @@ import { importFetchedAccounts, importFetchedStatus } from './importer'; import { favourite, unfavourite } from './interactions'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity, Status } from 'soapbox/types/entities'; +import type { APIEntity, EmojiReaction, Status } from 'soapbox/types/entities'; const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST'; const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS'; @@ -26,17 +26,17 @@ const noOp = () => () => new Promise(f => f(undefined)); const simpleEmojiReact = (status: Status, emoji: string, custom?: string) => (dispatch: AppDispatch) => { - const emojiReacts: ImmutableList> = status.pleroma.get('emoji_reactions') || ImmutableList(); + const emojiReacts: ImmutableList = status.reactions || ImmutableList(); if (emoji === '👍' && status.favourited) return dispatch(unfavourite(status)); - const undo = emojiReacts.filter(e => e.get('me') === true && e.get('name') === emoji).count() > 0; + const undo = emojiReacts.filter(e => e.me === true && e.name === emoji).count() > 0; if (undo) return dispatch(unEmojiReact(status, emoji)); return Promise.all([ ...emojiReacts - .filter((emojiReact) => emojiReact.get('me') === true) - .map(emojiReact => dispatch(unEmojiReact(status, emojiReact.get('name')))).toArray(), + .filter((emojiReact) => emojiReact.me === true) + .map(emojiReact => dispatch(unEmojiReact(status, emojiReact.name))).toArray(), status.favourited && dispatch(unfavourite(status)), ]).then(() => { if (emoji === '👍') { diff --git a/src/actions/mrf.ts b/src/actions/mrf.ts index 359d7711f..509bdb8fc 100644 --- a/src/actions/mrf.ts +++ b/src/actions/mrf.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable'; +import { Set as ImmutableSet } from 'immutable'; import ConfigDB from 'soapbox/utils/config-db'; @@ -7,9 +7,9 @@ import { fetchConfig, updateConfig } from './admin'; import type { MRFSimple } from 'soapbox/schemas/pleroma'; import type { AppDispatch, RootState } from 'soapbox/store'; -const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: ImmutableMap) => { +const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: Record) => { const entries = Object.entries(simplePolicy).map(([key, hosts]) => { - const isRestricted = restrictions.get(key); + const isRestricted = restrictions[key]; if (isRestricted) { return [key, ImmutableSet(hosts).add(host).toJS()]; @@ -21,7 +21,7 @@ const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: return Object.fromEntries(entries); }; -const updateMrf = (host: string, restrictions: ImmutableMap) => +const updateMrf = (host: string, restrictions: Record) => (dispatch: AppDispatch, getState: () => RootState) => dispatch(fetchConfig()) .then(() => { diff --git a/src/api/hooks/groups/useGroupValidation.ts b/src/api/hooks/groups/useGroupValidation.ts index cc1062ccc..470be22a5 100644 --- a/src/api/hooks/groups/useGroupValidation.ts +++ b/src/api/hooks/groups/useGroupValidation.ts @@ -41,7 +41,7 @@ function useGroupValidation(name: string = '') { ...queryInfo, data: { ...queryInfo.data, - isValid: !queryInfo.data?.error ?? true, + isValid: !queryInfo.data?.error, }, }; } diff --git a/src/api/hooks/nostr/useSignerStream.ts b/src/api/hooks/nostr/useSignerStream.ts index 6780351c8..216c3a8e3 100644 --- a/src/api/hooks/nostr/useSignerStream.ts +++ b/src/api/hooks/nostr/useSignerStream.ts @@ -36,7 +36,7 @@ function useSignerStream() { const respMsg = { id: reqMsg.data.id, - result: await signEvent(reqMsg.data.params[0]), + result: await signEvent(reqMsg.data.params[0], reqMsg.data.params[1]), }; const respEvent = await signEvent({ diff --git a/src/components/account.tsx b/src/components/account.tsx index ad4766ecd..cca7f47ce 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -187,7 +187,7 @@ const Account = ({ return (
- + = ({ title, subtitle, children }) => { return ( -
+
{title} {subtitle && {subtitle}} diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx new file mode 100644 index 000000000..234b62097 --- /dev/null +++ b/src/components/gameboy.tsx @@ -0,0 +1,192 @@ +// @ts-ignore No types available +import { WasmBoy } from '@soapbox.pub/wasmboy'; +import clsx from 'clsx'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import { exitFullscreen, isFullscreen, requestFullscreen } from 'soapbox/features/ui/util/fullscreen'; + +import { HStack, IconButton } from './ui'; + +let gainNode: GainNode | undefined; + +interface IGameboy extends Pick, 'onFocus' | 'onBlur'> { + /** Classname of the outer `
`. */ + className?: string; + /** URL to the ROM. */ + src: string; + /** Aspect ratio of the canvas. */ + aspect?: 'normal' | 'stretched'; +} + +/** Component to display a playable Gameboy emulator. */ +const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocus, onBlur, ...rest }) => { + const node = useRef(null); + const canvas = useRef(null); + + const [paused, setPaused] = useState(false); + const [muted, setMuted] = useState(true); + const [fullscreen, setFullscreen] = useState(false); + const [showControls, setShowControls] = useState(true); + + async function init() { + await WasmBoy.config(WasmBoyOptions, canvas.current!); + await WasmBoy.loadROM(src); + await play(); + + if (document.activeElement === canvas.current) { + await WasmBoy.enableDefaultJoypad(); + } else { + await WasmBoy.disableDefaultJoypad(); + } + } + + const handleFocus: React.FocusEventHandler = useCallback(() => { + WasmBoy.enableDefaultJoypad(); + }, []); + + const handleBlur: React.FocusEventHandler = useCallback(() => { + WasmBoy.disableDefaultJoypad(); + }, []); + + const handleFullscreenChange = useCallback(() => { + setFullscreen(isFullscreen()); + }, []); + + const handleCanvasClick = useCallback(() => { + setShowControls(!showControls); + }, [showControls]); + + const pause = async () => { + await WasmBoy.pause(); + setPaused(true); + }; + + const play = async () => { + await WasmBoy.play(); + setPaused(false); + }; + + const togglePaused = () => paused ? play() : pause(); + const toggleMuted = () => setMuted(!muted); + + const toggleFullscreen = () => { + if (isFullscreen()) { + exitFullscreen(); + } else if (node.current) { + requestFullscreen(node.current); + } + }; + + const handleDownload = () => { + window.open(src); + }; + + useEffect(() => { + init(); + + return () => { + WasmBoy.pause(); + WasmBoy.disableDefaultJoypad(); + }; + }, []); + + useEffect(() => { + document.addEventListener('fullscreenchange', handleFullscreenChange, true); + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange, true); + }; + }, []); + + useEffect(() => { + if (fullscreen) { + node.current?.focus(); + } + }, [fullscreen]); + + useEffect(() => { + if (gainNode) { + gainNode.gain.value = muted ? 0 : 1; + } + }, [gainNode, muted]); + + return ( +
+ + + + + + + + + + + + + +
+ ); +}; + +const WasmBoyOptions = { + headless: false, + useGbcWhenOptional: true, + isAudioEnabled: true, + frameSkip: 1, + audioBatchProcessing: true, + timersBatchProcessing: false, + audioAccumulateSamples: true, + graphicsBatchProcessing: false, + graphicsDisableScanlineRendering: false, + tileRendering: true, + tileCaching: true, + gameboyFPSCap: 60, + updateGraphicsCallback: false, + updateAudioCallback: (audioContext: AudioContext, audioBufferSourceNode: AudioBufferSourceNode) => { + gainNode = gainNode ?? audioContext.createGain(); + audioBufferSourceNode.connect(gainNode); + return gainNode; + }, + saveStateCallback: false, +}; + +export default Gameboy; \ No newline at end of file diff --git a/src/components/gdpr-banner.tsx b/src/components/gdpr-banner.tsx index 73d90684e..75bc16bad 100644 --- a/src/components/gdpr-banner.tsx +++ b/src/components/gdpr-banner.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Banner, Button, HStack, Stack, Text } from 'soapbox/components/ui'; -import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks'; +import { useInstance, useSoapboxConfig } from 'soapbox/hooks'; const acceptedGdpr = !!localStorage.getItem('soapbox:gdpr'); @@ -14,8 +14,7 @@ const GdprBanner: React.FC = () => { const [slideout, setSlideout] = useState(false); const instance = useInstance(); - const soapbox = useSoapboxConfig(); - const isLoggedIn = useAppSelector(state => !!state.me); + const { gdprUrl } = useSoapboxConfig(); const handleAccept = () => { localStorage.setItem('soapbox:gdpr', 'true'); @@ -23,15 +22,13 @@ const GdprBanner: React.FC = () => { setTimeout(() => setShown(true), 200); }; - const showBanner = soapbox.gdpr && !isLoggedIn && !shown; - - if (!showBanner) { + if (shown) { return null; } return ( -
+
@@ -47,8 +44,8 @@ const GdprBanner: React.FC = () => { - {soapbox.gdprUrl && ( - + {gdprUrl && ( + diff --git a/src/components/media-gallery.tsx b/src/components/media-gallery.tsx index 0d8e48683..368afc6e4 100644 --- a/src/components/media-gallery.tsx +++ b/src/components/media-gallery.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useState, useRef, useLayoutEffect } from 'react'; +import React, { useState, useRef, useLayoutEffect, Suspense } from 'react'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; @@ -15,6 +15,8 @@ import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maxi import type { Property } from 'csstype'; import type { List as ImmutableList } from 'immutable'; +const Gameboy = React.lazy(() => import('./gameboy')); + const ATTACHMENT_LIMIT = 4; const MAX_FILENAME_LENGTH = 45; @@ -141,8 +143,24 @@ const Item: React.FC = ({ } let thumbnail: React.ReactNode = ''; + const ext = attachment.url.split('.').pop()?.toLowerCase(); - if (attachment.type === 'unknown') { + if (attachment.type === 'unknown' && ['gb', 'gbc'].includes(ext!)) { + return ( +
1, + })} + key={attachment.id} + style={{ position, float, left, top, right, bottom, height, width: `${width}%` }} + > + }> + + +
+ ); + } else if (attachment.type === 'unknown') { const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH); const attachmentIcon = ( = ({
); } else if (attachment.type === 'audio') { - const ext = attachment.url.split('.').pop()?.toUpperCase(); thumbnail = ( = ({ title={attachment.description} > - {ext} + {ext} ); } else if (attachment.type === 'video') { - const ext = attachment.url.split('.').pop()?.toUpperCase(); thumbnail = ( = ({ > - {ext} + {ext} ); } diff --git a/src/components/polls/poll-option.tsx b/src/components/polls/poll-option.tsx index f27a81e64..760945ff9 100644 --- a/src/components/polls/poll-option.tsx +++ b/src/components/polls/poll-option.tsx @@ -122,7 +122,7 @@ const PollOption: React.FC = (props): JSX.Element | null => { return (
{showResults ? ( -
+
= ({ children }) => {
-

+

diff --git a/src/components/status-action-bar.tsx b/src/components/status-action-bar.tsx index aad3c1f82..b07550ed7 100644 --- a/src/components/status-action-bar.tsx +++ b/src/components/status-action-bar.tsx @@ -1,4 +1,3 @@ -import { List as ImmutableList } from 'immutable'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -626,15 +625,15 @@ const StatusActionBar: React.FC = ({ const reblogCount = status.reblogs_count; const favouriteCount = status.favourites_count; - const emojiReactCount = reduceEmoji( - (status.pleroma.get('emoji_reactions') || ImmutableList()) as ImmutableList, + const emojiReactCount = status.reactions ? reduceEmoji( + status.reactions, favouriteCount, status.favourited, allowedEmoji, - ).reduce((acc, cur) => acc + cur.get('count'), 0); + ).reduce((acc, cur) => acc + (cur.count || 0), 0) : undefined; const meEmojiReact = getReactForStatus(status, allowedEmoji); - const meEmojiName = meEmojiReact?.get('name') as keyof typeof reactMessages | undefined; + const meEmojiName = meEmojiReact?.name as keyof typeof reactMessages | undefined; const reactMessages = { '👍': messages.reactionLike, diff --git a/src/components/status-action-button.tsx b/src/components/status-action-button.tsx index 3dfe61939..aa87502ca 100644 --- a/src/components/status-action-button.tsx +++ b/src/components/status-action-button.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { Text, Icon, Emoji } from 'soapbox/components/ui'; import { shortNumberFormat } from 'soapbox/utils/numbers'; -import type { Map as ImmutableMap } from 'immutable'; +import type { EmojiReaction } from 'soapbox/schemas'; const COLORS = { accent: 'accent', @@ -33,7 +33,7 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes; + emoji?: EmojiReaction; text?: React.ReactNode; theme?: 'default' | 'inverse'; } @@ -45,7 +45,7 @@ const StatusActionButton = React.forwardRef - + ); } else { diff --git a/src/components/status-content.tsx b/src/components/status-content.tsx index 00b960815..c69459106 100644 --- a/src/components/status-content.tsx +++ b/src/components/status-content.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import parse, { Element, type HTMLReactParserOptions, domToReact } from 'html-react-parser'; +import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser'; import React, { useState, useRef, useLayoutEffect, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -105,7 +105,7 @@ const StatusContent: React.FC = ({ } if (classes?.includes('hashtag')) { - const child = domToReact(domNode.children); + const child = domToReact(domNode.children as DOMNode[]); const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined; if (hashtag) { return ; @@ -121,7 +121,7 @@ const StatusContent: React.FC = ({ target='_blank' title={domNode.attribs.href} > - {domToReact(domNode.children, options)} + {domToReact(domNode.children as DOMNode[], options)} ); } diff --git a/src/components/status-reaction-wrapper.tsx b/src/components/status-reaction-wrapper.tsx index f8e6129d1..c1d1224de 100644 --- a/src/components/status-reaction-wrapper.tsx +++ b/src/components/status-reaction-wrapper.tsx @@ -71,7 +71,7 @@ const StatusReactionWrapper: React.FC = ({ statusId, chi }; const handleClick: React.EventHandler = e => { - const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍'; + const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.name || '👍'; if (isUserTouching()) { if (ownAccount) { diff --git a/src/components/ui/input/input.tsx b/src/components/ui/input/input.tsx index e9510b7f5..bb8db57f3 100644 --- a/src/components/ui/input/input.tsx +++ b/src/components/ui/input/input.tsx @@ -93,7 +93,7 @@ const Input = React.forwardRef( 'text-gray-900 dark:text-gray-100': !props.disabled, 'text-gray-600': props.disabled, 'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal', - 'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white': theme === 'search', + 'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white dark:focus:bg-gray-900': theme === 'search', 'pr-10 rtl:pl-10 rtl:pr-3': isPassword || append, 'pl-8': typeof icon !== 'undefined', 'pl-16': typeof prepend !== 'undefined', diff --git a/src/components/ui/select/select.tsx b/src/components/ui/select/select.tsx index 888868a58..4c4c6c2c1 100644 --- a/src/components/ui/select/select.tsx +++ b/src/components/ui/select/select.tsx @@ -13,7 +13,7 @@ const Select = React.forwardRef((props, ref) => { = () => { if (providers.length > 0) { return ( - + diff --git a/src/features/chats/components/chat-page/chat-page.tsx b/src/features/chats/components/chat-page/chat-page.tsx index 16b5e2d0b..344b7a90a 100644 --- a/src/features/chats/components/chat-page/chat-page.tsx +++ b/src/features/chats/components/chat-page/chat-page.tsx @@ -60,7 +60,7 @@ const ChatPage: React.FC = ({ chatId }) => {

{isOnboarded ? (
= ({ chatId }) => { data-testid='chat-page' > diff --git a/src/features/chats/components/chat-page/components/chat-page-main.tsx b/src/features/chats/components/chat-page/components/chat-page-main.tsx index 89371c4da..6a9acd1e2 100644 --- a/src/features/chats/components/chat-page/components/chat-page-main.tsx +++ b/src/features/chats/components/chat-page/components/chat-page-main.tsx @@ -121,7 +121,7 @@ const ChatPageMain = () => { history.push('/chats')} /> diff --git a/src/features/chats/components/chat-page/components/chat-page-new.tsx b/src/features/chats/components/chat-page/components/chat-page-new.tsx index 0a60c56b4..ed9535efd 100644 --- a/src/features/chats/components/chat-page/components/chat-page-new.tsx +++ b/src/features/chats/components/chat-page/components/chat-page-new.tsx @@ -24,7 +24,7 @@ const ChatPageNew: React.FC = () => { history.push('/chats')} /> diff --git a/src/features/chats/components/chat-page/components/chat-page-settings.tsx b/src/features/chats/components/chat-page/components/chat-page-settings.tsx index cb637a36e..d042b75ac 100644 --- a/src/features/chats/components/chat-page/components/chat-page-settings.tsx +++ b/src/features/chats/components/chat-page/components/chat-page-settings.tsx @@ -51,7 +51,7 @@ const ChatPageSettings = () => { history.push('/chats')} /> diff --git a/src/features/chats/components/chat-textarea.tsx b/src/features/chats/components/chat-textarea.tsx index a8269b0c0..f85d897d5 100644 --- a/src/features/chats/components/chat-textarea.tsx +++ b/src/features/chats/components/chat-textarea.tsx @@ -39,9 +39,9 @@ const ChatTextarea: React.FC = React.forwardRef(({ bg-white text-gray-900 shadow-sm placeholder:text-gray-600 focus-within:border-primary-500 - focus-within:ring-1 focus-within:ring-primary-500 dark:border-gray-800 dark:bg-gray-800 - dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus-within:border-primary-500 - dark:focus-within:ring-primary-500 sm:text-sm + focus-within:ring-1 focus-within:ring-primary-500 sm:text-sm dark:border-gray-800 + dark:bg-gray-800 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 + dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500 `} > {(!!attachments?.length || isUploading) && ( diff --git a/src/features/compose/components/compose-form.tsx b/src/features/compose/components/compose-form.tsx index 456dab69a..45771dd94 100644 --- a/src/features/compose/components/compose-form.tsx +++ b/src/features/compose/components/compose-form.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor } from 'lexical'; +import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor, $getRoot } from 'lexical'; import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Link, useHistory } from 'react-router-dom'; @@ -18,7 +18,6 @@ import { Button, HStack, Stack } from 'soapbox/components/ui'; import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container'; import { ComposeEditor } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch, useAppSelector, useCompose, useDraggedFiles, useFeatures, useInstance, usePrevious } from 'soapbox/hooks'; -import { isMobile } from 'soapbox/is-mobile'; import QuotedStatusContainer from '../containers/quoted-status-container'; import ReplyIndicatorContainer from '../containers/reply-indicator-container'; @@ -96,23 +95,25 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab const anyMedia = compose.media_attachments.size > 0; const [composeFocused, setComposeFocused] = useState(false); - const [text, setText] = useState(compose.text); const firstRender = useRef(true); const formRef = useRef(null); const spoilerTextRef = useRef(null); const editorRef = useRef(null); - const { isDraggedOver } = useDraggedFiles(formRef); + const text = editorRef.current?.getEditorState().read(() => $getRoot().getTextContent()) ?? ''; + const fulltext = [spoilerText, countableText(text)].join(''); + + const isEmpty = !(fulltext.trim() || anyMedia); + const condensed = shouldCondense && !isDraggedOver && !composeFocused && isEmpty && !isUploading; + const shouldAutoFocus = autoFocus && !showSearch; + const canSubmit = !!editorRef.current && !isSubmitting && !isUploading && !isChangingUpload && !isEmpty && length(fulltext) <= maxTootChars; + const getClickableArea = () => { return clickableAreaRef ? clickableAreaRef.current : formRef.current; }; - const isEmpty = () => { - return !(text || spoilerText || anyMedia); - }; - const isClickOutside = (e: MouseEvent | React.MouseEvent) => { return ![ // List of elements that shouldn't collapse the composer when clicked @@ -125,10 +126,10 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab }; const handleClick = useCallback((e: MouseEvent | React.MouseEvent) => { - if (isEmpty() && isClickOutside(e)) { + if (isEmpty && isClickOutside(e)) { handleClickOutside(); } - }, []); + }, [isEmpty]); const handleClickOutside = () => { setComposeFocused(false); @@ -139,20 +140,12 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab }; const handleSubmit = (e?: React.FormEvent) => { + if (!canSubmit) return; + e?.preventDefault(); + dispatch(changeCompose(id, text)); - - // Submit disabled: - const fulltext = [spoilerText, countableText(text)].join(''); - - if (e) { - e.preventDefault(); - } - - if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxTootChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) { - return; - } - dispatch(submitCompose(id, { history })); + editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); }; @@ -215,12 +208,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab ), [features, id]); - const condensed = shouldCondense && !isDraggedOver && !composeFocused && isEmpty() && !isUploading; - const disabled = isSubmitting; - const countedText = [spoilerText, countableText(text)].join(''); - const disabledButton = disabled || isUploading || isChangingUpload || length(countedText) > maxTootChars || (countedText.length !== 0 && countedText.trim().length === 0 && !anyMedia); - const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth); - const composeModifiers = !condensed && ( @@ -297,7 +284,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab autoFocus={shouldAutoFocus} hasPoll={hasPoll} handleSubmit={handleSubmit} - onChange={setText} onFocus={handleComposeFocus} onPaste={onPaste} /> @@ -324,7 +310,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab )} -
diff --git a/src/features/compose/editor/plugins/focus-plugin.tsx b/src/features/compose/editor/plugins/focus-plugin.tsx index 65b567683..8a076f199 100644 --- a/src/features/compose/editor/plugins/focus-plugin.tsx +++ b/src/features/compose/editor/plugins/focus-plugin.tsx @@ -11,10 +11,6 @@ export const FOCUS_EDITOR_COMMAND: LexicalCommand = createCommand(); const FocusPlugin: React.FC = ({ autoFocus }) => { const [editor] = useLexicalComposerContext(); - const focus = () => { - editor.dispatchCommand(FOCUS_EDITOR_COMMAND, undefined); - }; - useEffect(() => editor.registerCommand(FOCUS_EDITOR_COMMAND, () => { editor.focus( () => { @@ -29,8 +25,10 @@ const FocusPlugin: React.FC = ({ autoFocus }) => { }, COMMAND_PRIORITY_NORMAL)); useEffect(() => { - if (autoFocus) focus(); - }, []); + if (autoFocus) { + editor.dispatchCommand(FOCUS_EDITOR_COMMAND, undefined); + } + }, [autoFocus, editor]); return null; }; diff --git a/src/features/edit-profile/components/header-picker.tsx b/src/features/edit-profile/components/header-picker.tsx index 13f555844..3176876c6 100644 --- a/src/features/edit-profile/components/header-picker.tsx +++ b/src/features/edit-profile/components/header-picker.tsx @@ -36,7 +36,7 @@ const HeaderPicker = React.forwardRef(({ src, onC