diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 32d8f2d0..5b2a87df 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -20,7 +20,7 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v3.5.3 - name: Discover build-time variables run: | @@ -37,11 +37,12 @@ jobs: ;; esac - - name: Login to Docker Hub + - name: Login to registry uses: docker/login-action@v2 with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} if: github.event_name != 'pull_request' - name: Build and push @@ -54,4 +55,4 @@ jobs: context: . file: Containerfile push: ${{ github.event_name != 'pull_request' }} - tags: retrospring/retrospring:${{ env.RETROSPRING_VERSION }} + tags: ghcr.io/retrospring/retrospring:${{ env.RETROSPRING_VERSION }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7238441c..2d920b7f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.5.3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4916381e..ae94f520 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,70 +11,102 @@ jobs: name: Rubocop runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v3.5.3 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v37 + with: + files: "**/*.rb" - name: Install dependencies run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev + if: steps.changed-files.outputs.any_changed == 'true' - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true + if: steps.changed-files.outputs.any_changed == 'true' - name: Run rubocop uses: reviewdog/action-rubocop@v2 with: rubocop_version: gemfile rubocop_extensions: rubocop-rails:gemfile reporter: github-pr-check + if: steps.changed-files.outputs.any_changed == 'true' eslint: name: ESLint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v3.5.3 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v37 + with: + files: "**/*.ts" - name: Set up Node 14 uses: actions/setup-node@v3 with: node-version: '14' cache: 'yarn' + if: steps.changed-files.outputs.any_changed == 'true' - name: Install node modules run: | npm i -g yarn yarn install --frozen-lockfile + if: steps.changed-files.outputs.any_changed == 'true' - uses: reviewdog/action-eslint@v1 with: reporter: github-check eslint_flags: '--ext .ts app/javascript' + if: steps.changed-files.outputs.any_changed == 'true' haml-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v3.5.3 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v37 + with: + files: "**/*.haml" - name: Install dependencies run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev + if: steps.changed-files.outputs.any_changed == 'true' - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true - - uses: patch-technology/action-haml-lint@0.4 + if: steps.changed-files.outputs.any_changed == 'true' + - uses: patch-technology/action-haml-lint@0.5 with: reporter: github-check rubocop_version: gemfile + if: steps.changed-files.outputs.any_changed == 'true' stylelint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v3.5.3 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v37 + with: + files: "**/*.scss" - name: Set up Node 14 uses: actions/setup-node@v3 with: node-version: '14' cache: 'yarn' + if: steps.changed-files.outputs.any_changed == 'true' - name: Install node modules run: | npm i -g yarn yarn install --frozen-lockfile + if: steps.changed-files.outputs.any_changed == 'true' - name: stylelint - uses: pixeldesu/action-stylelint@5ec750b03a94da735352bdb02e9dfc3d5af33aba + uses: reviewdog/action-stylelint@v1.17.1 with: github_token: ${{ secrets.github_token }} reporter: github-pr-check stylelint_input: 'app/assets/stylesheets/**/*.scss' + if: steps.changed-files.outputs.any_changed == 'true' diff --git a/.github/workflows/retrospring.yml b/.github/workflows/retrospring.yml index d22a2768..cc3c0c32 100644 --- a/.github/workflows/retrospring.yml +++ b/.github/workflows/retrospring.yml @@ -41,7 +41,7 @@ jobs: BUNDLE_WITHOUT: 'production' steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v3.5.3 - name: Install dependencies run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev - name: Set up Ruby diff --git a/.rubocop.yml b/.rubocop.yml index acd3db74..ed74d1c9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -134,3 +134,6 @@ Style/EndlessMethod: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: consistent_comma diff --git a/Containerfile b/Containerfile index 2e740498..700b458e 100644 --- a/Containerfile +++ b/Containerfile @@ -2,6 +2,11 @@ FROM registry.opensuse.org/opensuse/leap:15.4 +LABEL org.opencontainers.image.title="Retrospring (production)" +LABEL org.opencontainers.image.description="Image containing everything to run Retrospring in production mode. Do not use this for development." +LABEL org.opencontainers.image.vendor="The Retrospring team" +LABEL org.opencontainers.image.url="https://github.com/Retrospring/retrospring" + ARG RETROSPRING_VERSION=2023.0131.1 ARG RUBY_VERSION=3.1.2 ARG RUBY_INSTALL_VERSION=0.9.0 diff --git a/Gemfile b/Gemfile index a831e984..d3d2c537 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ gem "i18n-js", "4.0" gem "rails", "~> 6.1" gem "rails-i18n", "~> 7.0" -gem "cssbundling-rails", "~> 1.1" +gem "cssbundling-rails", "~> 1.2" gem "jsbundling-rails", "~> 1.1" gem "sassc-rails" gem "sprockets", "~> 4.1" @@ -16,7 +16,7 @@ gem "pg" gem "turbo-rails" -gem "bcrypt", "~> 3.1.18" +gem "bcrypt", "~> 3.1.19" gem "active_model_otp" gem "bootsnap", require: false @@ -24,7 +24,7 @@ gem "bootstrap_form", "~> 5.0" gem "carrierwave", "~> 2.0" gem "carrierwave_backgrounder", git: "https://github.com/raccube/carrierwave_backgrounder.git" gem "colorize" -gem "devise", "~> 4.0" +gem "devise", "~> 4.9" gem "devise-async" gem "devise-i18n" gem "fog-aws" @@ -42,8 +42,6 @@ gem "rolify", "~> 6.0" gem "dry-initializer", "~> 3.1" gem "dry-types", "~> 1.7" -gem "ruby-progressbar" - gem "pghero" gem "rails_admin" gem "sentry-rails" @@ -92,8 +90,8 @@ group :development, :test do gem "rspec-mocks" gem "rspec-rails", "~> 6.0" gem "rspec-sidekiq", "~> 3.0", require: false - gem "rubocop", "~> 1.45" - gem "rubocop-rails", "~> 2.17" + gem "rubocop", "~> 1.55" + gem "rubocop-rails", "~> 2.20" gem "shoulda-matchers", "~> 5.3" gem "simplecov", require: false gem "simplecov-cobertura", require: false @@ -118,4 +116,4 @@ gem "openssl", "~> 3.1" # mail 2.8.0 breaks sendmail usage: https://github.com/mikel/mail/issues/1538 gem "mail", "~> 2.7.1" -gem "prometheus-client", "~> 4.0" +gem "prometheus-client", "~> 4.2" diff --git a/Gemfile.lock b/Gemfile.lock index 5e5b0320..191e99d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,80 +15,80 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.2) - actionpack (= 6.1.7.2) - activesupport (= 6.1.7.2) + actioncable (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.2) - actionpack (= 6.1.7.2) - activejob (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + actionmailbox (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (>= 2.7.1) - actionmailer (6.1.7.2) - actionpack (= 6.1.7.2) - actionview (= 6.1.7.2) - activejob (= 6.1.7.2) - activesupport (= 6.1.7.2) + actionmailer (6.1.7.4) + actionpack (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.2) - actionview (= 6.1.7.2) - activesupport (= 6.1.7.2) + actionpack (6.1.7.4) + actionview (= 6.1.7.4) + activesupport (= 6.1.7.4) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.2) - actionpack (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + actiontext (6.1.7.4) + actionpack (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) nokogiri (>= 1.8.5) - actionview (6.1.7.2) - activesupport (= 6.1.7.2) + actionview (6.1.7.4) + activesupport (= 6.1.7.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_model_otp (2.3.1) + active_model_otp (2.3.2) activemodel rotp (~> 6.2.0) - activejob (6.1.7.2) - activesupport (= 6.1.7.2) + activejob (6.1.7.4) + activesupport (= 6.1.7.4) globalid (>= 0.3.6) - activemodel (6.1.7.2) - activesupport (= 6.1.7.2) + activemodel (6.1.7.4) + activesupport (= 6.1.7.4) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (6.1.7.2) - activemodel (= 6.1.7.2) - activesupport (= 6.1.7.2) - activestorage (6.1.7.2) - actionpack (= 6.1.7.2) - activejob (= 6.1.7.2) - activerecord (= 6.1.7.2) - activesupport (= 6.1.7.2) + activerecord (6.1.7.4) + activemodel (= 6.1.7.4) + activesupport (= 6.1.7.4) + activestorage (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activesupport (= 6.1.7.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.2) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) - bcrypt (3.1.18) - better_errors (2.9.1) - coderay (>= 1.0.0) + bcrypt (3.1.19) + better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) + rouge (>= 1.0.0) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) bootsnap (1.16.0) @@ -108,22 +108,21 @@ GEM mimemagic (>= 0.3.0) mini_mime (>= 0.1.3) chunky_png (1.4.0) - coderay (1.1.3) - colorize (0.8.1) - concurrent-ruby (1.2.0) - connection_pool (2.3.0) + colorize (1.1.0) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) crass (1.0.6) - cssbundling-rails (1.1.2) + cssbundling-rails (1.2.0) railties (>= 6.0.0) - database_cleaner (2.0.1) - database_cleaner-active_record (~> 2.0.0) - database_cleaner-active_record (2.0.1) + database_cleaner (2.0.2) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.3) debug_inspector (1.1.0) - devise (4.8.1) + devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -132,7 +131,7 @@ GEM devise-async (1.0.0) activejob (>= 5.0) devise (>= 4.0) - devise-i18n (1.10.2) + devise-i18n (1.10.3) devise (>= 4.8.0) diff-lcs (1.5.0) docile (1.4.0) @@ -141,15 +140,15 @@ GEM zeitwerk (~> 2.6) dry-inflector (1.0.0) dry-initializer (3.1.1) - dry-logic (1.4.0) + dry-logic (1.5.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-types (1.7.0) + dry-types (1.7.1) concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) - dry-inflector (~> 1.0, < 2) - dry-logic (>= 1.4, < 2) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) zeitwerk (~> 2.6) erubi (1.12.0) excon (0.99.0) @@ -164,7 +163,7 @@ GEM faker (3.1.1) i18n (>= 1.8.11, < 2) ffi (1.15.5) - fog-aws (3.17.0) + fog-aws (3.19.0) fog-core (~> 2.1) fog-json (~> 1.1) fog-xml (~> 0.1) @@ -189,11 +188,11 @@ GEM temple (>= 0.8.2) thor tilt - haml_lint (0.45.0) + haml_lint (0.49.2) haml (>= 4.0, < 6.2) parallel (~> 1.10) rainbow - rubocop (>= 0.50.0) + rubocop (>= 1.0) sysexits (~> 1.1) hcaptcha (7.1.0) json @@ -202,7 +201,7 @@ GEM httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) i18n-js (4.0.0) glob @@ -211,12 +210,12 @@ GEM image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - jsbundling-rails (1.1.1) + jsbundling-rails (1.1.2) railties (>= 6.0.0) json (2.6.3) - json-schema (3.0.0) + json-schema (4.0.0) addressable (>= 2.8) - jwt (2.7.0) + jwt (2.7.1) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -229,32 +228,33 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + language_server-protocol (3.17.0.3) launchy (2.5.0) addressable (~> 2.7) letter_opener (1.8.1) launchy (>= 2.2, < 3) - lograge (0.12.0) + lograge (0.13.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.19.1) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) mail (2.7.1) mini_mime (>= 0.1.1) marcel (1.0.2) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.0218.1) mimemagic (0.4.3) nokogiri (~> 1) rake mini_magick (4.12.0) mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.17.0) + mini_portile2 (2.8.4) + minitest (5.19.0) msgpack (1.6.0) multi_json (1.15.0) multi_xml (0.6.0) @@ -263,7 +263,7 @@ GEM connection_pool (~> 2.2) net-http2 (0.18.4) http-2 (~> 0.11) - net-imap (0.3.4) + net-imap (0.3.7) date net-protocol net-pop (0.1.2) @@ -272,65 +272,68 @@ GEM timeout net-smtp (0.3.3) net-protocol - nio4r (2.5.8) - nokogiri (1.14.1) - mini_portile2 (~> 2.8.0) + nio4r (2.5.9) + nokogiri (1.15.3) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.14.2) + oj (3.15.1) openssl (3.1.0) orm_adapter (0.5.0) - parallel (1.22.1) - parser (3.2.1.0) + parallel (1.23.0) + parser (3.2.2.3) ast (~> 2.4.1) - pg (1.4.5) - pghero (3.1.0) + racc + pg (1.5.3) + pghero (3.3.3) activerecord (>= 6) - prometheus-client (4.0.0) - public_suffix (4.0.7) - puma (6.1.0) + prometheus-client (4.2.0) + public_suffix (5.0.1) + puma (6.3.0) nio4r (~> 2.0) - pundit (2.3.0) + pundit (2.3.1) activesupport (>= 3.0.0) - racc (1.6.2) - rack (2.2.6.2) - rack-test (2.0.2) + racc (1.7.1) + rack (2.2.8) + rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.2) - actioncable (= 6.1.7.2) - actionmailbox (= 6.1.7.2) - actionmailer (= 6.1.7.2) - actionpack (= 6.1.7.2) - actiontext (= 6.1.7.2) - actionview (= 6.1.7.2) - activejob (= 6.1.7.2) - activemodel (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + rails (6.1.7.4) + actioncable (= 6.1.7.4) + actionmailbox (= 6.1.7.4) + actionmailer (= 6.1.7.4) + actionpack (= 6.1.7.4) + actiontext (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activemodel (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) bundler (>= 1.15.0) - railties (= 6.1.7.2) + railties (= 6.1.7.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.1.1) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) - rails-i18n (7.0.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + rails-i18n (7.0.7) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - rails_admin (3.1.1) + rails_admin (3.1.2) activemodel-serializers-xml (>= 1.0) kaminari (>= 0.14, < 2.0) nested_form (~> 0.3) rails (>= 6.0, < 8) turbo-rails (~> 1.0) - railties (6.1.7.2) - actionpack (= 6.1.7.2) - activesupport (= 6.1.7.2) + railties (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) method_source rake (>= 12.2) thor (~> 1.0) @@ -338,15 +341,16 @@ GEM rake (13.0.6) redcarpet (3.6.0) redis (4.8.0) - regexp_parser (2.7.0) + regexp_parser (2.8.1) request_store (1.5.1) rack (>= 1.4) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) - rexml (3.2.5) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.2.6) rolify (6.0.1) - rotp (6.2.0) + rotp (6.2.2) + rouge (4.1.2) rpush (7.0.1) activesupport (>= 5.2) jwt (>= 1.5.6) @@ -357,54 +361,55 @@ GEM rainbow thor (>= 0.18.1, < 2.0) webpush (~> 1.0) - rqrcode (2.1.2) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.12.0) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.12.3) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.1) + rspec-rails (6.0.3) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.11) - rspec-expectations (~> 3.11) - rspec-mocks (~> 3.11) - rspec-support (~> 3.11) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.12.0) - rubocop (1.45.1) + rspec-support (3.12.1) + rubocop (1.55.1) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.24.1, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.26.0) + rubocop-ast (1.29.0) parser (>= 3.2.1.0) - rubocop-rails (2.17.4) + rubocop-rails (2.20.2) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - ruby-progressbar (1.11.0) + ruby-progressbar (1.13.0) ruby-vips (2.1.4) ffi (~> 1.12) rubyzip (2.3.2) - sanitize (6.0.1) + sanitize (6.0.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) sassc (2.4.0) @@ -415,13 +420,13 @@ GEM sprockets (> 3.0) sprockets-rails tilt - sentry-rails (5.8.0) + sentry-rails (5.10.0) railties (>= 5.0) - sentry-ruby (~> 5.8.0) - sentry-ruby (5.8.0) + sentry-ruby (~> 5.10.0) + sentry-ruby (5.10.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.8.0) - sentry-ruby (~> 5.8.0) + sentry-sidekiq (5.10.0) + sentry-ruby (~> 5.10.0) sidekiq (>= 3.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) @@ -450,14 +455,14 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) sysexits (1.2.0) - temple (0.10.0) - thor (1.2.1) - tilt (2.0.11) - timeout (0.3.1) + temple (0.10.2) + thor (1.2.2) + tilt (2.2.0) + timeout (0.4.0) tldv (0.1.0) tldv-data (~> 1.0) - tldv-data (1.0.2022121701) - turbo-rails (1.3.3) + tldv-data (1.0.2023031000) + turbo-rails (1.4.0) actionpack (>= 6.0.0) activejob (>= 6.0.0) railties (>= 6.0.0) @@ -479,14 +484,14 @@ GEM websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.6) + zeitwerk (2.6.10) PLATFORMS ruby DEPENDENCIES active_model_otp - bcrypt (~> 3.1.18) + bcrypt (~> 3.1.19) better_errors binding_of_caller bootsnap @@ -496,9 +501,9 @@ DEPENDENCIES carrierwave_backgrounder! colorize connection_pool - cssbundling-rails (~> 1.1) + cssbundling-rails (~> 1.2) database_cleaner - devise (~> 4.0) + devise (~> 4.9) devise-async devise-i18n dry-initializer (~> 3.1) @@ -528,7 +533,7 @@ DEPENDENCIES openssl (~> 3.1) pg pghero - prometheus-client (~> 4.0) + prometheus-client (~> 4.2) puma pundit (~> 2.3) questiongenerator (~> 1.2)! @@ -546,9 +551,8 @@ DEPENDENCIES rspec-mocks rspec-rails (~> 6.0) rspec-sidekiq (~> 3.0) - rubocop (~> 1.45) - rubocop-rails (~> 2.17) - ruby-progressbar + rubocop (~> 1.55) + rubocop-rails (~> 2.20) rubyzip (~> 2.3) sanitize sassc-rails diff --git a/app/assets/stylesheets/application.sass.scss b/app/assets/stylesheets/application.sass.scss index 43f030dd..e1464641 100644 --- a/app/assets/stylesheets/application.sass.scss +++ b/app/assets/stylesheets/application.sass.scss @@ -100,26 +100,27 @@ $unicodeRangeValues in Lexend.$unicodeMap { */ @import "components/announcements", -"components/answerbox", -"components/avatars", -"components/buttons", -"components/collapse", -"components/comments", -"components/container", -"components/entry", -"components/icons", -"components/inbox-actions", -"components/inbox-entry", -"components/mobile-nav", -"components/navbar", -"components/notifications", -"components/profile", -"components/push-settings", -"components/question", -"components/smiles", -"components/themes", -"components/totp-setup", -"components/userbox"; + "components/answerbox", + "components/avatars", + "components/buttons", + "components/collapse", + "components/comments", + "components/container", + "components/entry", + "components/hotkey", + "components/icons", + "components/inbox-actions", + "components/inbox-entry", + "components/mobile-nav", + "components/navbar", + "components/notifications", + "components/profile", + "components/push-settings", + "components/question", + "components/smiles", + "components/themes", + "components/totp-setup", + "components/userbox"; /** UTILITIES diff --git a/app/assets/stylesheets/components/_comments.scss b/app/assets/stylesheets/components/_comments.scss index fed1e673..9b0ff50d 100644 --- a/app/assets/stylesheets/components/_comments.scss +++ b/app/assets/stylesheets/components/_comments.scss @@ -24,18 +24,20 @@ } &__input { - padding-right: 2.5rem; - &.is-invalid { background-image: none; } } - &__character-count { - position: absolute; - z-index: 5; - right: .5rem; - top: .5rem; + &__compose-wrapper { + display: flex; + } + + &__submit-wrapper { + display: flex; + flex-direction: column; + text-align: center; + margin-left: 0.5rem; } } diff --git a/app/assets/stylesheets/components/_hotkey.scss b/app/assets/stylesheets/components/_hotkey.scss new file mode 100644 index 00000000..777f1113 --- /dev/null +++ b/app/assets/stylesheets/components/_hotkey.scss @@ -0,0 +1,6 @@ +.js-hotkey-navigating { + + .js-hotkey-current-selection { + outline: var(--primary) solid 4px; + } +} diff --git a/app/assets/stylesheets/components/_totp-setup.scss b/app/assets/stylesheets/components/_totp-setup.scss index 2bcf341b..9856151a 100644 --- a/app/assets/stylesheets/components/_totp-setup.scss +++ b/app/assets/stylesheets/components/_totp-setup.scss @@ -47,6 +47,13 @@ &__recovery { &-container { max-width: 455px; + + @media print { + + .card { + box-shadow: none; + } + } } &-icon { @@ -73,4 +80,4 @@ #user_otp_attempt { @extend %totp-input; -} \ No newline at end of file +} diff --git a/app/controllers/ajax/answer_controller.rb b/app/controllers/ajax/answer_controller.rb index 859ae794..0c401638 100644 --- a/app/controllers/ajax/answer_controller.rb +++ b/app/controllers/ajax/answer_controller.rb @@ -5,6 +5,7 @@ require "cgi" class Ajax::AnswerController < AjaxController include SocialHelper::TwitterMethods include SocialHelper::TumblrMethods + include SocialHelper::TelegramMethods def create params.require :id @@ -41,19 +42,13 @@ class Ajax::AnswerController < AjaxController @response[:message] = t(".success") @response[:success] = true - if current_user.sharing_enabled - @response[:sharing] = { - twitter: twitter_share_url(answer), - tumblr: tumblr_share_url(answer), - custom: CGI.escape(prepare_tweet(answer)) - } - end + @response[:sharing] = sharing_hash(answer) if current_user.sharing_enabled return if inbox # this assign is needed because shared/_answerbox relies on it, I think @question = 1 - @response[:render] = render_to_string(partial: "answerbox", locals: { a: answer, show_question: false }) + @response[:render] = render_to_string(partial: "answerbox", locals: { a: answer, show_question: false, subscribed_answer_ids: [answer.id] }) end def destroy @@ -74,4 +69,13 @@ class Ajax::AnswerController < AjaxController @response[:message] = t(".success") @response[:success] = true end + + private + + def sharing_hash(answer) = { + twitter: twitter_share_url(answer), + tumblr: tumblr_share_url(answer), + telegram: telegram_share_url(answer), + custom: CGI.escape(prepare_tweet(answer)), + } end diff --git a/app/controllers/ajax/subscription_controller.rb b/app/controllers/ajax/subscription_controller.rb index a04520e4..fd574e78 100644 --- a/app/controllers/ajax/subscription_controller.rb +++ b/app/controllers/ajax/subscription_controller.rb @@ -4,14 +4,14 @@ class Ajax::SubscriptionController < AjaxController def subscribe params.require :answer @response[:status] = :okay - state = Subscription.subscribe(current_user, Answer.find(params[:answer])).nil? - @response[:success] = state == false + result = Subscription.subscribe(current_user, Answer.find(params[:answer])) + @response[:success] = result.present? end def unsubscribe params.require :answer @response[:status] = :okay - state = Subscription.unsubscribe(current_user, Answer.find(params[:answer])).nil? - @response[:success] = state == false + result = Subscription.unsubscribe(current_user, Answer.find(params[:answer])) + @response[:success] = result&.destroyed? || false end end diff --git a/app/controllers/answer_controller.rb b/app/controllers/answer_controller.rb index d5e70881..ac52ab40 100644 --- a/app/controllers/answer_controller.rb +++ b/app/controllers/answer_controller.rb @@ -10,17 +10,12 @@ class AnswerController < ApplicationController def show @answer = Answer.includes(comments: %i[user smiles], question: [:user], smiles: [:user]).find(params[:id]) @display_all = true + @subscribed_answer_ids = [] - if user_signed_in? - notif = Notification.where(type: "Notification::QuestionAnswered", target_id: @answer.id, recipient_id: current_user.id, new: true).first - notif&.update(new: false) - notif = Notification.where(type: "Notification::Commented", target_id: @answer.comments.pluck(:id), recipient_id: current_user.id, new: true) - notif.update_all(new: false) unless notif.empty? - notif = Notification.where(type: "Notification::Smiled", target_id: @answer.smiles.pluck(:id), recipient_id: current_user.id, new: true) - notif.update_all(new: false) unless notif.empty? - notif = Notification.where(type: "Notification::CommentSmiled", target_id: @answer.comment_smiles.pluck(:id), recipient_id: current_user.id, new: true) - notif.update_all(new: false) unless notif.empty? - end + return unless user_signed_in? + + @subscribed_answer_ids = Subscription.where(user: current_user, answer: @answer).pluck(:answer_id) + mark_notifications_as_read end def pin @@ -52,4 +47,15 @@ class AnswerController < ApplicationController end end end + + private + + def mark_notifications_as_read + Notification.where(recipient_id: current_user.id, new: true) + .and(Notification.where(type: "Notification::QuestionAnswered", target_id: @answer.id) + .or(Notification.where(type: "Notification::Commented", target_id: @answer.comments.pluck(:id))) + .or(Notification.where(type: "Notification::Smiled", target_id: @answer.smiles.pluck(:id))) + .or(Notification.where(type: "Notification::CommentSmiled", target_id: @answer.comment_smiles.pluck(:id)))) + .update_all(new: false) # rubocop:disable Rails/SkipsModelValidations + end end diff --git a/app/controllers/concerns/paginates_answers.rb b/app/controllers/concerns/paginates_answers.rb new file mode 100644 index 00000000..2d5081c5 --- /dev/null +++ b/app/controllers/concerns/paginates_answers.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module PaginatesAnswers + def paginate_answers + @answers = yield(last_id: params[:last_id]) + answer_ids = @answers.map(&:id) + @answers_last_id = answer_ids.min + answer_ids += @pinned_answers.pluck(:id) if @pinned_answers.present? + @more_data_available = !yield(last_id: @answers_last_id, size: 1).count.zero? + @subscribed_answer_ids = Subscription.where(user: current_user, answer_id: answer_ids).pluck(:answer_id) if user_signed_in? + end +end diff --git a/app/controllers/discover_controller.rb b/app/controllers/discover_controller.rb index 7f415c78..2e1be8b4 100644 --- a/app/controllers/discover_controller.rb +++ b/app/controllers/discover_controller.rb @@ -13,6 +13,9 @@ class DiscoverController < ApplicationController @popular_questions = Question.where("created_at > ?", Time.now.ago(1.week)).order(:answer_count).reverse_order.limit(top_x).includes(:user) @new_users = User.where("asked_count > 0").order(:id).reverse_order.limit(top_x).includes(:profile) + answer_ids = @popular_answers.map(&:id) + @most_discussed.map(&:id) + @subscribed_answer_ids = Subscription.where(user: current_user, answer_id: answer_ids).pluck(:answer_id) + # .user = the user # .question_count = how many questions did the user ask @users_with_most_questions = Question.select('user_id, COUNT(*) AS question_count'). diff --git a/app/controllers/inbox_controller.rb b/app/controllers/inbox_controller.rb index f6a1aff2..661d49f4 100644 --- a/app/controllers/inbox_controller.rb +++ b/app/controllers/inbox_controller.rb @@ -82,10 +82,13 @@ class InboxController < ApplicationController .where(questions: { user: @author_user, author_is_anonymous: false }) end + # rubocop:disable Rails/SkipsModelValidations def mark_inbox_entries_as_read # using .dup to not modify @inbox -- useful in tests - @inbox&.dup&.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations + @inbox&.dup&.update_all(new: false) + current_user.touch(:inbox_updated_at) end + # rubocop:enable Rails/SkipsModelValidations def increment_metric Retrospring::Metrics::QUESTIONS_ASKED.increment( diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 860dd3ec..b0088074 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -25,6 +25,17 @@ class NotificationsController < ApplicationController end end + def read + current_user.notifications.where(new: true).update_all(new: false) # rubocop:disable Rails/SkipsModelValidations + current_user.touch(:notifications_updated_at) + + respond_to do |format| + format.turbo_stream do + render "navigation/notifications", locals: { notifications: [], notification_count: nil } + end + end + end + private def paginate_notifications @@ -38,10 +49,13 @@ class NotificationsController < ApplicationController .count(:target_type) end + # rubocop:disable Rails/SkipsModelValidations def mark_notifications_as_read # using .dup to not modify @notifications -- useful in tests - @notifications&.dup&.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations + @notifications&.dup&.update_all(new: false) + current_user.touch(:notifications_updated_at) end + # rubocop:enable Rails/SkipsModelValidations def cursored_notifications_for(type:, last_id:, size: nil) cursor_params = { last_id: last_id, size: size }.compact diff --git a/app/controllers/question_controller.rb b/app/controllers/question_controller.rb index f8a3ffa1..14dbe80e 100644 --- a/app/controllers/question_controller.rb +++ b/app/controllers/question_controller.rb @@ -1,9 +1,15 @@ +# frozen_string_literal: true + class QuestionController < ApplicationController + include PaginatesAnswers + def show @question = Question.find(params[:id]) - @answers = @question.cursored_answers(last_id: params[:last_id]) - @answers_last_id = @answers.map(&:id).min - @more_data_available = !@question.cursored_answers(last_id: @answers_last_id, size: 1).count.zero? + @answers = @question.cursored_answers(last_id: params[:last_id], current_user:) + answer_ids = @answers.map(&:id) + @answers_last_id = answer_ids.min + @more_data_available = !@question.cursored_answers(last_id: @answers_last_id, size: 1, current_user:).count.zero? + @subscribed = Subscription.where(user: current_user, answer_id: answer_ids).pluck(:answer_id) if user_signed_in? respond_to do |format| format.html diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index ff83ad4d..75fb4251 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -42,6 +42,6 @@ class Settings::TwoFactorAuthentication::OtpAuthenticationController < Applicati def reset current_user.totp_recovery_codes.delete_all @recovery_keys = TotpRecoveryCode.generate_for(current_user) - render "settings/two_factor_authentication/otp_authentication/recovery_keys" + render "settings/two_factor_authentication/otp_authentication/recovery_keys", status: :see_other end end diff --git a/app/controllers/timeline_controller.rb b/app/controllers/timeline_controller.rb index 14a27a5c..6b34fdb4 100644 --- a/app/controllers/timeline_controller.rb +++ b/app/controllers/timeline_controller.rb @@ -11,12 +11,12 @@ class TimelineController < ApplicationController def list @title = list_title(@list) - paginate_timeline { |args| @list.cursored_timeline(**args) } + paginate_timeline { |args| @list.cursored_timeline(**args, current_user:) } end def public @title = generate_title(t(".title")) - paginate_timeline { |args| Answer.cursored_public_timeline(**args) } + paginate_timeline { |args| Answer.cursored_public_timeline(**args, current_user:) } end private @@ -32,8 +32,10 @@ class TimelineController < ApplicationController def paginate_timeline @timeline = yield(last_id: params[:last_id]) - @timeline_last_id = @timeline.map(&:id).min + timeline_ids = @timeline.map(&:id) + @timeline_last_id = timeline_ids.min @more_data_available = !yield(last_id: @timeline_last_id, size: 1).count.zero? + @subscribed_answer_ids = Subscription.where(user: current_user, answer_id: timeline_ids).pluck(:answer_id) respond_to do |format| format.html { render "timeline/timeline" } diff --git a/app/controllers/user/sessions_controller.rb b/app/controllers/user/sessions_controller.rb index 8829e017..ff6c9b9e 100644 --- a/app/controllers/user/sessions_controller.rb +++ b/app/controllers/user/sessions_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User::SessionsController < Devise::SessionsController def new session.delete(:user_sign_in_uid) @@ -5,35 +7,13 @@ class User::SessionsController < Devise::SessionsController end def create - if session.has_key?(:user_sign_in_uid) - self.resource = User.find(session.delete(:user_sign_in_uid)) - else - self.resource = warden.authenticate!(auth_options) - end + authenticate! if resource.active_for_authentication? && resource.otp_module_enabled? if params[:user][:otp_attempt].blank? - session[:user_sign_in_uid] = resource.id - sign_out(resource) - warden.lock! - render "auth/two_factor_authentication" + prompt_for_2fa else - if params[:user][:otp_attempt].length == 8 - found = TotpRecoveryCode.where(user_id: resource.id, code: params[:user][:otp_attempt].downcase).delete_all - if found == 1 - flash[:info] = t(".info", count: TotpRecoveryCode.where(user_id: resource.id).count) - continue_sign_in(resource, resource_name) - else - flash[:error] = t(".error") - redirect_to new_user_session_url - end - elsif resource.authenticate_otp(params[:user][:otp_attempt], drift: APP_CONFIG.fetch(:otp_drift_period, 30).to_i) - continue_sign_in(resource, resource_name) - else - sign_out(resource) - flash[:error] = t(".error") - redirect_to new_user_session_url - end + attempt_2fa end else continue_sign_in(resource, resource_name) @@ -42,10 +22,47 @@ class User::SessionsController < Devise::SessionsController private + def authenticate! + self.resource = session.key?(:user_sign_in_uid) ? User.find(session.delete(:user_sign_in_uid)) : warden.authenticate!(auth_options) + end + def continue_sign_in(resource, resource_name) set_flash_message!(:notice, :signed_in) sign_in(resource_name, resource) yield resource if block_given? respond_with resource, location: after_sign_in_path_for(resource) end -end \ No newline at end of file + + def prompt_for_2fa + session[:user_sign_in_uid] = resource.id + sign_out(resource) + warden.lock! + render "auth/two_factor_authentication" + end + + def attempt_2fa + if params[:user][:otp_attempt].length == 8 + try_recovery_code + elsif resource.authenticate_otp(params[:user][:otp_attempt], drift: APP_CONFIG.fetch(:otp_drift_period, 30).to_i) + continue_sign_in(resource, resource_name) + else + fail_2fa + end + end + + def try_recovery_code + found = TotpRecoveryCode.where(user_id: resource.id, code: params[:user][:otp_attempt].downcase).delete_all + if found == 1 + flash[:info] = t(".info", count: TotpRecoveryCode.where(user_id: resource.id).count) + continue_sign_in(resource, resource_name) + else + fail_2fa + end + end + + def fail_2fa + sign_out(resource) + flash[:error] = t(".error") + redirect_to new_user_session_url + end +end diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index 1963b496..7b9084b9 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true class UserController < ApplicationController + include PaginatesAnswers + before_action :set_user before_action :hidden_social_graph_redirect, only: %i[followers followings] after_action :mark_notification_as_read, only: %i[show] def show - @answers = @user.cursored_answers(last_id: params[:last_id]) @pinned_answers = @user.answers.pinned.order(pinned_at: :desc).limit(10) - @answers_last_id = @answers.map(&:id).min - @more_data_available = !@user.cursored_answers(last_id: @answers_last_id, size: 1).count.zero? + paginate_answers { |args| @user.cursored_answers(**args) } respond_to do |format| format.html - format.turbo_stream + format.turbo_stream { render layout: false } end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5c69fa9c..b8cbeb15 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -4,30 +4,6 @@ module ApplicationHelper include ApplicationHelper::GraphMethods include ApplicationHelper::TitleMethods - def inbox_count - return 0 unless user_signed_in? - - count = Inbox.select("COUNT(id) AS count") - .where(new: true) - .where(user_id: current_user.id) - .group(:user_id) - .order(:count) - .first - return nil if count.nil? - return nil unless count.count.positive? - - count.count - end - - def notification_count - return 0 unless user_signed_in? - - count = Notification.for(current_user).where(new: true).count - return nil unless count.positive? - - count - end - def privileged?(user) !current_user.nil? && ((current_user == user) || current_user.mod?) end diff --git a/app/helpers/bootstrap_helper.rb b/app/helpers/bootstrap_helper.rb index 9445de93..4d14e3dc 100644 --- a/app/helpers/bootstrap_helper.rb +++ b/app/helpers/bootstrap_helper.rb @@ -5,8 +5,10 @@ module BootstrapHelper options = { badge: nil, badge_color: nil, + badge_attr: {}, icon: nil, - class: "" + class: "", + hotkey: nil, }.merge(options) classes = [ @@ -22,17 +24,17 @@ module BootstrapHelper "#{content_tag(:i, '', class: "fa fa-#{options[:icon]}")} #{body}" end end - unless options[:badge].nil? + if options[:badge].present? || options.dig(:badge_attr, :data)&.has_key?(:controller) badge_class = [ "badge", ("badge-#{options[:badge_color]}" unless options[:badge_color].nil?), ("badge-pill" if options[:badge_pill]) ].compact.join(" ") - body += " #{content_tag(:span, options[:badge], class: badge_class)}".html_safe + body += " #{content_tag(:span, options[:badge], class: badge_class, **options[:badge_attr])}".html_safe end - content_tag(:li, link_to(body.html_safe, path, class: "nav-link"), class: classes) + content_tag(:li, link_to(body.html_safe, path, class: "nav-link", data: { hotkey: options[:hotkey] }), class: classes) end def list_group_item(body, path, options = {}) diff --git a/app/helpers/social_helper.rb b/app/helpers/social_helper.rb index 00447583..e1ffe35f 100644 --- a/app/helpers/social_helper.rb +++ b/app/helpers/social_helper.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + module SocialHelper include SocialHelper::TwitterMethods include SocialHelper::TumblrMethods -end \ No newline at end of file + include SocialHelper::TelegramMethods +end diff --git a/app/helpers/social_helper/telegram_methods.rb b/app/helpers/social_helper/telegram_methods.rb new file mode 100644 index 00000000..ec13f9ea --- /dev/null +++ b/app/helpers/social_helper/telegram_methods.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "cgi" + +module SocialHelper::TelegramMethods + include MarkdownHelper + + def telegram_text(answer) + # using twitter_markdown here as it removes all formatting + "#{twitter_markdown answer.question.content}\nā€”ā€”ā€”\n#{twitter_markdown answer.content}" + end + + def telegram_share_url(answer) + url = answer_url( + id: answer.id, + username: answer.user.screen_name, + host: APP_CONFIG["hostname"], + protocol: (APP_CONFIG["https"] ? :https : :http) + ) + + %(https://t.me/share/url?url=#{CGI.escape(url)}&text=#{CGI.escape(telegram_text(answer))}) + end +end diff --git a/app/helpers/user_helper.rb b/app/helpers/user_helper.rb index a494a4d1..0f027e52 100644 --- a/app/helpers/user_helper.rb +++ b/app/helpers/user_helper.rb @@ -12,7 +12,7 @@ module UserHelper if url return user_path(user) if link_only - return profile_link(user) + return profile_link(user, target: "_top") end user.profile.safe_name.strip end @@ -23,8 +23,8 @@ module UserHelper private - def profile_link(user) - link_to(user.profile.safe_name, user_path(user), class: ("user--banned" if user.banned?).to_s) + def profile_link(user, target: nil) + link_to(user.profile.safe_name, user_path(user), class: ("user--banned" if user.banned?).to_s, target:) end def should_unmask?(author_identifier) diff --git a/app/javascript/retrospring/common.ts b/app/javascript/retrospring/common.ts index 811e9a8a..bc5f3ec6 100644 --- a/app/javascript/retrospring/common.ts +++ b/app/javascript/retrospring/common.ts @@ -1,10 +1,14 @@ import '@hotwired/turbo-rails'; import initializeBootstrap from './initializers/bootstrap'; +import initializeHotkey from './initializers/hotkey'; +import initializeServiceWorker from './initializers/serviceWorker'; import initializeStimulus from './initializers/stimulus'; export default function start(): void { try { initializeBootstrap(); + initializeHotkey(); + initializeServiceWorker(); initializeStimulus(); } catch (e) { // initialization errors diff --git a/app/javascript/retrospring/controllers/hotkey_controller.ts b/app/javascript/retrospring/controllers/hotkey_controller.ts new file mode 100644 index 00000000..2b776fe6 --- /dev/null +++ b/app/javascript/retrospring/controllers/hotkey_controller.ts @@ -0,0 +1,12 @@ +import { Controller } from "@hotwired/stimulus"; +import { install, uninstall } from "@github/hotkey"; + +export default class extends Controller { + connect(): void { + install(this.element); + } + + disconnect(): void { + uninstall(this.element); + } +} diff --git a/app/javascript/retrospring/controllers/inbox_sharing_controller.ts b/app/javascript/retrospring/controllers/inbox_sharing_controller.ts index 48f0a905..6d0218b1 100644 --- a/app/javascript/retrospring/controllers/inbox_sharing_controller.ts +++ b/app/javascript/retrospring/controllers/inbox_sharing_controller.ts @@ -1,10 +1,11 @@ import { Controller } from '@hotwired/stimulus'; export default class extends Controller { - static targets = ['twitter', 'tumblr', 'custom']; + static targets = ['twitter', 'tumblr', 'telegram', 'custom']; declare readonly twitterTarget: HTMLAnchorElement; declare readonly tumblrTarget: HTMLAnchorElement; + declare readonly telegramTarget: HTMLAnchorElement; declare readonly customTarget: HTMLAnchorElement; declare readonly hasCustomTarget: boolean; @@ -20,6 +21,7 @@ export default class extends Controller { if (this.autoCloseValue) { this.twitterTarget.addEventListener('click', () => this.close()); this.tumblrTarget.addEventListener('click', () => this.close()); + this.telegramTarget.addEventListener('click', () => this.close()); if (this.hasCustomTarget) { this.customTarget.addEventListener('click', () => this.close()); @@ -36,6 +38,7 @@ export default class extends Controller { this.twitterTarget.href = this.configValue['twitter']; this.tumblrTarget.href = this.configValue['tumblr']; + this.telegramTarget.href = this.configValue['telegram']; if (this.hasCustomTarget) { this.customTarget.href = `${this.customTarget.href}${this.configValue['custom']}`; diff --git a/app/javascript/retrospring/controllers/navigation_controller.ts b/app/javascript/retrospring/controllers/navigation_controller.ts new file mode 100644 index 00000000..b95445eb --- /dev/null +++ b/app/javascript/retrospring/controllers/navigation_controller.ts @@ -0,0 +1,62 @@ +import { Controller } from "@hotwired/stimulus"; +import { install, uninstall } from "@github/hotkey"; + +export default class extends Controller { + static classes = ["current"]; + static targets = ["current", "traversable"]; + + declare readonly hasCurrentTarget: boolean; + declare readonly currentTarget: HTMLElement; + declare readonly traversableTargets: HTMLElement[]; + + traversableTargetConnected(target: HTMLElement): void { + if (!("navigationIndex" in target.dataset)) { + target.dataset.navigationIndex = this.traversableTargets.indexOf(target).toString(); + } + + if (!this.hasCurrentTarget) { + const first = this.traversableTargets[0]; + first.dataset.navigationTarget += " current"; + } + } + + currentTargetConnected(target: HTMLElement): void { + target.classList.add("js-hotkey-current-selection"); + + target.querySelectorAll("[data-selection-hotkey]") + .forEach(el => install(el, el.dataset.selectionHotkey)); + } + + currentTargetDisconnected(target: HTMLElement): void { + target.classList.remove("js-hotkey-current-selection"); + + target.querySelectorAll("[data-selection-hotkey]") + .forEach(el => uninstall(el)); + } + + up(): void { + const prevIndex = this.traversableTargets.indexOf(this.currentTarget) - 1; + if (prevIndex == -1) return; + + this.navigate(this.traversableTargets[prevIndex]); + } + + down(): void { + const nextIndex = this.traversableTargets.indexOf(this.currentTarget) + 1; + if (nextIndex == this.traversableTargets.length) return; + + this.navigate(this.traversableTargets[nextIndex]); + } + + navigate(target: HTMLElement): void { + if (!document.body.classList.contains("js-hotkey-navigating")) { + document.body.classList.add("js-hotkey-navigating"); + } + + if (target.dataset.navigationTarget == "traversable") { + this.currentTarget.dataset.navigationTarget = "traversable"; + target.dataset.navigationTarget = "traversable current"; + target.scrollIntoView({ block: "center", inline: "center" }); + } + } +} diff --git a/app/javascript/retrospring/controllers/pwa_badge_controller.ts b/app/javascript/retrospring/controllers/pwa_badge_controller.ts new file mode 100644 index 00000000..491d0216 --- /dev/null +++ b/app/javascript/retrospring/controllers/pwa_badge_controller.ts @@ -0,0 +1,18 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + isPwa: boolean; + badgeCapable: boolean; + + initialize(): void { + this.isPwa = window.matchMedia('(display-mode: standalone)').matches; + this.badgeCapable = "setAppBadge" in navigator; + } + + connect(): void { + if (this.isPwa && this.badgeCapable) { + const count = Number.parseInt(this.element.innerText) || 0; + navigator.setAppBadge(count); + } + } +} diff --git a/app/javascript/retrospring/features/answerbox/comment/hotkey.ts b/app/javascript/retrospring/features/answerbox/comment/hotkey.ts new file mode 100644 index 00000000..0e15dd7f --- /dev/null +++ b/app/javascript/retrospring/features/answerbox/comment/hotkey.ts @@ -0,0 +1,7 @@ +export function commentHotkeyHandler(event: Event): void { + const button = event.target as HTMLButtonElement; + const id = button.dataset.aId; + + document.querySelector(`#ab-comments-section-${id}`).classList.remove('d-none'); + document.querySelector(`[name="ab-comment-new"][data-a-id="${id}"]`).focus(); +} diff --git a/app/javascript/retrospring/features/answerbox/comment/index.ts b/app/javascript/retrospring/features/answerbox/comment/index.ts index 5168fd57..3e5a0f81 100644 --- a/app/javascript/retrospring/features/answerbox/comment/index.ts +++ b/app/javascript/retrospring/features/answerbox/comment/index.ts @@ -1,18 +1,21 @@ import registerEvents from "retrospring/utilities/registerEvents"; import { commentDestroyHandler } from "./destroy"; -import {commentComposeEnd, commentComposeStart, commentCreateHandler} from "./new"; +import { commentComposeEnd, commentComposeStart, commentCreateClickHandler, commentCreateKeyboardHandler } from "./new"; import { commentReportHandler } from "./report"; import { commentSmileHandler } from "./smile"; import { commentToggleHandler } from "./toggle"; +import { commentHotkeyHandler } from "retrospring/features/answerbox/comment/hotkey"; export default (): void => { registerEvents([ { type: 'click', target: '[name=ab-comments]', handler: commentToggleHandler, global: true }, + { type: 'click', target: '[name=ab-open-and-comment]', handler: commentHotkeyHandler, global: true }, { type: 'click', target: '[name=ab-smile-comment]', handler: commentSmileHandler, global: true }, { type: 'click', target: '[data-action=ab-comment-report]', handler: commentReportHandler, global: true }, { type: 'click', target: '[data-action=ab-comment-destroy]', handler: commentDestroyHandler, global: true }, { type: 'compositionstart', target: '[name=ab-comment-new]', handler: commentComposeStart, global: true }, { type: 'compositionend', target: '[name=ab-comment-new]', handler: commentComposeEnd, global: true }, - { type: 'keyup', target: '[name=ab-comment-new]', handler: commentCreateHandler, global: true } + { type: 'keydown', target: '[name=ab-comment-new]', handler: commentCreateKeyboardHandler, global: true }, + { type: 'click', target: '[name=ab-comment-new-submit]', handler: commentCreateClickHandler, global: true } ]); } diff --git a/app/javascript/retrospring/features/answerbox/comment/new.ts b/app/javascript/retrospring/features/answerbox/comment/new.ts index 7bbb8043..154448e7 100644 --- a/app/javascript/retrospring/features/answerbox/comment/new.ts +++ b/app/javascript/retrospring/features/answerbox/comment/new.ts @@ -5,7 +5,50 @@ import { showNotification, showErrorNotification } from 'utilities/notifications let compositionJustEnded = false; -export function commentCreateHandler(event: KeyboardEvent): boolean { +function createComment(input: HTMLInputElement, id: string, counter: Element, group: Element) { + if (input.value.length > 512) { + group.classList.add('has-error'); + return true; + } + + input.disabled = true; + + post('/ajax/create_comment', { + body: { + answer: id, + comment: input.value + }, + contentType: 'application/json' + }) + .then(async response => { + const data = await response.json; + + if (data.success) { + document.querySelector(`#ab-comments-${id}`).innerHTML = data.render; + const commentCount = document.getElementById(`#ab-comment-count-${id}`); + if (commentCount) { + commentCount.innerHTML = data.count; + } + input.value = ''; + counter.innerHTML = String(512); + + const sub = document.querySelector(`[data-action=ab-submarine][data-a-id="${id}"]`); + sub.dataset.torpedo = "no" + sub.children[0].nextSibling.textContent = ' ' + I18n.translate('voc.unsubscribe'); + } + + showNotification(data.message, data.success); + }) + .catch(err => { + console.log(err); + showErrorNotification(I18n.translate('frontend.error.message')); + }) + .finally(() => { + input.disabled = false; + }); +} + +export function commentCreateKeyboardHandler(event: KeyboardEvent): boolean { if (compositionJustEnded && event.which == 13) { compositionJustEnded = false; return; @@ -16,52 +59,25 @@ export function commentCreateHandler(event: KeyboardEvent): boolean { const counter = document.querySelector(`#ab-comment-charcount-${id}`); const group = document.querySelector(`[name=ab-comment-new-group][data-a-id="${id}"]`); - if (event.which === 13) { + if ((event.ctrlKey || event.metaKey) && event.which === 13) { event.preventDefault(); - if (input.value.length > 512) { - group.classList.add('has-error'); - return true; - } - - input.disabled = true; - - post('/ajax/create_comment', { - body: { - answer: id, - comment: input.value - }, - contentType: 'application/json' - }) - .then(async response => { - const data = await response.json; - - if (data.success) { - document.querySelector(`#ab-comments-${id}`).innerHTML = data.render; - const commentCount = document.getElementById(`#ab-comment-count-${id}`); - if (commentCount) { - commentCount.innerHTML = data.count; - } - input.value = ''; - counter.innerHTML = String(512); - - const sub = document.querySelector(`[data-action=ab-submarine][data-a-id="${id}"]`); - sub.dataset.torpedo = "no" - sub.children[0].nextSibling.textContent = ' ' + I18n.translate('voc.unsubscribe'); - } - - showNotification(data.message, data.success); - }) - .catch(err => { - console.log(err); - showErrorNotification(I18n.translate('frontend.error.message')); - }) - .finally(() => { - input.disabled = false; - }); + createComment(input, id, counter, group); } } +export function commentCreateClickHandler(event: MouseEvent): void { + event.preventDefault(); + + const button = event.target as HTMLButtonElement; + const id = button.dataset.aId; + const input = document.querySelector(`[name="ab-comment-new"][data-a-id="${id}"]`); + const counter = document.querySelector(`#ab-comment-charcount-${id}`); + const group = document.querySelector(`[name=ab-comment-new-group][data-a-id="${id}"]`); + + createComment(input, id, counter, group); +} + export function commentComposeStart(): boolean { compositionJustEnded = false; return true; diff --git a/app/javascript/retrospring/features/webpush/enable.ts b/app/javascript/retrospring/features/webpush/enable.ts index 332a76ec..c92aed64 100644 --- a/app/javascript/retrospring/features/webpush/enable.ts +++ b/app/javascript/retrospring/features/webpush/enable.ts @@ -8,7 +8,7 @@ export function enableHandler (event: Event): void { const sender = event.target as HTMLButtonElement; try { - installServiceWorker() + getServiceWorker() .then(subscribe) .then(async subscription => { return Notification.requestPermission().then(permission => { @@ -51,8 +51,8 @@ export function enableHandler (event: Event): void { } } -async function installServiceWorker(): Promise { - return navigator.serviceWorker.register("/service_worker.js", { scope: "/" }); +async function getServiceWorker(): Promise { + return navigator.serviceWorker.getRegistration("/"); } async function getServerKey(): Promise { diff --git a/app/javascript/retrospring/initializers/hotkey.ts b/app/javascript/retrospring/initializers/hotkey.ts new file mode 100644 index 00000000..d57ddcf9 --- /dev/null +++ b/app/javascript/retrospring/initializers/hotkey.ts @@ -0,0 +1,7 @@ +import { install } from '@github/hotkey' + +export default function (): void { + document.addEventListener('turbo:load', () => { + document.querySelectorAll('[data-hotkey]').forEach(el => install(el as HTMLElement)); + }); +} diff --git a/app/javascript/retrospring/initializers/serviceWorker.ts b/app/javascript/retrospring/initializers/serviceWorker.ts new file mode 100644 index 00000000..fdf5f1ff --- /dev/null +++ b/app/javascript/retrospring/initializers/serviceWorker.ts @@ -0,0 +1,3 @@ +export default function (): void { + navigator.serviceWorker.register("/service_worker.js", { scope: "/" }); +} diff --git a/app/javascript/retrospring/initializers/stimulus.ts b/app/javascript/retrospring/initializers/stimulus.ts index 6239a1f9..aee44e36 100644 --- a/app/javascript/retrospring/initializers/stimulus.ts +++ b/app/javascript/retrospring/initializers/stimulus.ts @@ -8,8 +8,11 @@ import CollapseController from "retrospring/controllers/collapse_controller"; import ThemeController from "retrospring/controllers/theme_controller"; import CapabilitiesController from "retrospring/controllers/capabilities_controller"; import CropperController from "retrospring/controllers/cropper_controller"; +import HotkeyController from "retrospring/controllers/hotkey_controller"; import InboxSharingController from "retrospring/controllers/inbox_sharing_controller"; import ToastController from "retrospring/controllers/toast_controller"; +import PwaBadgeController from "retrospring/controllers/pwa_badge_controller"; +import NavigationController from "retrospring/controllers/navigation_controller"; /** * This module sets up Stimulus and our controllers @@ -28,7 +31,10 @@ export default function (): void { window['Stimulus'].register('collapse', CollapseController); window['Stimulus'].register('cropper', CropperController); window['Stimulus'].register('format-popup', FormatPopupController); + window['Stimulus'].register('hotkey', HotkeyController); window['Stimulus'].register('inbox-sharing', InboxSharingController); + window['Stimulus'].register('pwa-badge', PwaBadgeController); + window['Stimulus'].register('navigation', NavigationController); window['Stimulus'].register('theme', ThemeController); window['Stimulus'].register('toast', ToastController); } diff --git a/app/models/answer.rb b/app/models/answer.rb index d1e3e645..5e43464b 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -1,8 +1,8 @@ class Answer < ApplicationRecord extend Answer::TimelineMethods - belongs_to :user - belongs_to :question + belongs_to :user, counter_cache: :answered_count + belongs_to :question, counter_cache: :answer_count has_many :comments, dependent: :destroy has_many :smiles, class_name: "Appendable::Reaction", foreign_key: :parent_id, dependent: :destroy has_many :subscriptions, dependent: :destroy @@ -18,15 +18,12 @@ class Answer < ApplicationRecord SHORT_ANSWER_MAX_LENGTH = 640 - # rubocop:disable Rails/SkipsModelValidations after_create do Inbox.where(user: self.user, question: self.question).destroy_all Notification.notify self.question.user, self unless self.question.user == self.user or self.question.user.nil? Subscription.subscribe self.user, self Subscription.subscribe self.question.user, self unless self.question.author_is_anonymous - user.increment! :answered_count - question.increment! :answer_count end before_destroy do @@ -39,19 +36,15 @@ class Answer < ApplicationRecord end end - user&.decrement! :answered_count - question&.decrement! :answer_count self.smiles.each do |smile| Notification.denotify self.user, smile end self.comments.each do |comment| - comment.user&.decrement! :commented_count Subscription.denotify comment, self end Notification.denotify question&.user, self Subscription.destruct self end - # rubocop:enable Rails/SkipsModelValidations def notification_type(*_args) Notification::QuestionAnswered diff --git a/app/models/answer/timeline_methods.rb b/app/models/answer/timeline_methods.rb index 732ba2bd..8c6085bf 100644 --- a/app/models/answer/timeline_methods.rb +++ b/app/models/answer/timeline_methods.rb @@ -5,8 +5,21 @@ module Answer::TimelineMethods define_cursor_paginator :cursored_public_timeline, :public_timeline - def public_timeline + def public_timeline(current_user: nil) joins(:user) + .then do |query| + next query unless current_user + + blocked_and_muted_user_ids = current_user.blocked_user_ids_cached + current_user.muted_user_ids_cached + next query if blocked_and_muted_user_ids.empty? + + # build a more complex query if we block or mute someone + # otherwise the query ends up as "anon OR (NOT anon AND user_id NOT IN (NULL))" which will only return anonymous questions + query + .joins(:question) + .where("questions.author_is_anonymous OR (NOT questions.author_is_anonymous AND questions.user_id NOT IN (?))", blocked_and_muted_user_ids) + .where.not(answers: { user_id: blocked_and_muted_user_ids }) + end .where(users: { privacy_allow_public_timeline: true }) .order(:created_at) .reverse_order diff --git a/app/models/comment.rb b/app/models/comment.rb index 7e982170..00472037 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,18 +1,15 @@ class Comment < ApplicationRecord - belongs_to :user - belongs_to :answer + belongs_to :user, counter_cache: :commented_count + belongs_to :answer, counter_cache: :comment_count validates :user_id, presence: true validates :answer_id, presence: true has_many :smiles, class_name: "Appendable::Reaction", foreign_key: :parent_id, dependent: :destroy validates :content, length: { maximum: 512 } - # rubocop:disable Rails/SkipsModelValidations after_create do - Subscription.subscribe self.user, answer, false + Subscription.subscribe user, answer Subscription.notify self, answer - user.increment! :commented_count - answer.increment! :comment_count end before_destroy do @@ -25,10 +22,7 @@ class Comment < ApplicationRecord end Subscription.denotify self, answer - user&.decrement! :commented_count - answer&.decrement! :comment_count end - # rubocop:enable Rails/SkipsModelValidations def notification_type(*_args) Notification::Commented diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 8c8c602f..6a74687f 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Inbox < ApplicationRecord - belongs_to :user + belongs_to :user, touch: :inbox_updated_at belongs_to :question attr_accessor :returning @@ -27,9 +27,10 @@ class Inbox < ApplicationRecord self.destroy end + def notification_icon = question.author_is_anonymous ? "/icons/maskable_icon_x128.png" : question.user.profile_picture.url(:small) + def as_push_notification { - type: :inbox, title: I18n.t( "frontend.push_notifications.inbox.title", user: if question.author_is_anonymous @@ -38,8 +39,11 @@ class Inbox < ApplicationRecord question.user.profile.safe_name end ), - icon: question.author_is_anonymous ? "/icons/maskable_icon_x128.png" : question.user.profile_picture.url(:small), - body: question.content.truncate(Question::SHORT_QUESTION_MAX_LENGTH) + icon: notification_icon, + body: question.content.truncate(Question::SHORT_QUESTION_MAX_LENGTH), + data: { + click_url: "/inbox", + }, } end end diff --git a/app/models/list/timeline_methods.rb b/app/models/list/timeline_methods.rb index 3cf68e5e..7909d544 100644 --- a/app/models/list/timeline_methods.rb +++ b/app/models/list/timeline_methods.rb @@ -6,7 +6,21 @@ module List::TimelineMethods define_cursor_paginator :cursored_timeline, :timeline # @return [Array] the lists' timeline - def timeline - Answer.where('user_id in (?)', members.pluck(:user_id)).order(:created_at).reverse_order + def timeline(current_user: nil) + Answer + .then do |query| + next query unless current_user + + blocked_and_muted_user_ids = current_user.blocked_user_ids_cached + current_user.muted_user_ids_cached + next query if blocked_and_muted_user_ids.empty? + + # build a more complex query if we block or mute someone + # otherwise the query ends up as "anon OR (NOT anon AND user_id NOT IN (NULL))" which will only return anonymous questions + query + .joins(:question) + .where("questions.author_is_anonymous OR (NOT questions.author_is_anonymous AND questions.user_id NOT IN (?))", blocked_and_muted_user_ids) + .where.not(answers: { user_id: blocked_and_muted_user_ids }) + end + .where(answers: { user_id: members.pluck(:user_id) }).order(:created_at).reverse_order end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 60890d17..3e15eae4 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Notification < ApplicationRecord - belongs_to :recipient, class_name: "User" + belongs_to :recipient, class_name: "User", touch: :notifications_updated_at belongs_to :target, polymorphic: true class << self @@ -11,11 +11,11 @@ class Notification < ApplicationRecord define_cursor_paginator :cursored_for_type, :for_type def for(recipient, **kwargs) - where(kwargs.merge!(recipient:)).includes(:target).order(:created_at).reverse_order + where(kwargs.merge!(recipient:)).order(:created_at).reverse_order end def for_type(recipient, type, **kwargs) - where(kwargs.merge!(recipient:)).includes(:target).where(type:).order(:created_at).reverse_order + where(kwargs.merge!(recipient:)).where(type:).order(:created_at).reverse_order end def notify(recipient, target) diff --git a/app/models/question/answer_methods.rb b/app/models/question/answer_methods.rb index 2de7f3b2..efb94a7b 100644 --- a/app/models/question/answer_methods.rb +++ b/app/models/question/answer_methods.rb @@ -5,8 +5,17 @@ module Question::AnswerMethods define_cursor_paginator :cursored_answers, :ordered_answers - def ordered_answers + def ordered_answers(current_user: nil) answers + .then do |query| + next query unless current_user + + blocked_and_muted_user_ids = current_user.blocked_user_ids_cached + current_user.muted_user_ids_cached + next query if blocked_and_muted_user_ids.empty? + + query + .where.not(answers: { user_id: blocked_and_muted_user_ids }) + end .order(:created_at) .reverse_order end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index bab3bf39..0b05a1b9 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,73 +1,52 @@ +# frozen_string_literal: true + class Subscription < ApplicationRecord belongs_to :user belongs_to :answer class << self - def for(target) - Subscription.where(answer: target) - end - - def is_subscribed(recipient, target) + def subscribe(recipient, target) existing = Subscription.find_by(user: recipient, answer: target) - if existing.nil? - false - else - existing.is_active - end - end + return true if existing.present? - def subscribe(recipient, target, force = true) - existing = Subscription.find_by(user: recipient, answer: target) - if existing.nil? - Subscription.new(user: recipient, answer: target).save! - elsif force - existing.update(is_active: true) - end + Subscription.create!(user: recipient, answer: target) end def unsubscribe(recipient, target) - if recipient.nil? or target.nil? - return nil - end + return nil if recipient.nil? || target.nil? subs = Subscription.find_by(user: recipient, answer: target) - subs.update(is_active: false) unless subs.nil? + subs&.destroy end def destruct(target) - if target.nil? - return nil - end + return nil if target.nil? + Subscription.where(answer: target).destroy_all end - def destruct_by(recipient, target) - if recipient.nil? or target.nil? - return nil - end - - subs = Subscription.find_by(user: recipient, answer: target) - subs.destroy unless subs.nil? - end - def notify(source, target) - if source.nil? or target.nil? - return nil + return nil if source.nil? || target.nil? + + muted_by = Relationships::Mute.where(target: source.user).pluck(&:source_id) + + # As we will need to notify for each person subscribed, + # it's much faster to bulk insert than to use +Notification.notify+ + notifications = Subscription.where(answer: target) + .where.not(user: source.user) + .where.not(user_id: muted_by) + .map do |s| + { target_id: source.id, target_type: Comment, recipient_id: s.user_id, new: true, type: Notification::Commented, created_at: source.created_at, updated_at: source.created_at } end - Subscription.where(answer: target, is_active: true).each do |subs| - next unless not subs.user == source.user - Notification.notify subs.user, source - end + Notification.insert_all!(notifications) unless notifications.empty? # rubocop:disable Rails/SkipsModelValidations end def denotify(source, target) - if source.nil? or target.nil? - return nil - end - Subscription.where(answer: target).each do |subs| - Notification.denotify subs.user, source - end + return nil if source.nil? || target.nil? + + subs = Subscription.where(answer: target) + Notification.where(target:, recipient: subs.map(&:user)).delete_all end end end diff --git a/app/models/user.rb b/app/models/user.rb index 725582ef..2b2241e0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,6 +8,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength include User::AnswerMethods include User::BanMethods include User::InboxMethods + include User::NotificationMethods include User::QuestionMethods include User::PushNotificationMethods include User::ReactionMethods @@ -84,8 +85,14 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength end end + after_destroy do + Retrospring::Metrics::USERS_DESTROYED.increment + end + after_create do Profile.create(user_id: id) if Profile.where(user_id: id).count.zero? + + Retrospring::Metrics::USERS_CREATED.increment end # use the screen name as parameter for url helpers diff --git a/app/models/user/inbox_methods.rb b/app/models/user/inbox_methods.rb index 1d961ce2..8e3b00fe 100644 --- a/app/models/user/inbox_methods.rb +++ b/app/models/user/inbox_methods.rb @@ -12,4 +12,18 @@ module User::InboxMethods .order(:created_at) .reverse_order end + + def unread_inbox_count + Rails.cache.fetch(inbox_cache_key) do + count = Inbox.where(new: true, user_id: id).count(:id) + + # Returning +nil+ here in order to not display a counter + # at all when there isn't anything in the user's inbox + return nil unless count.positive? + + count + end + end + + def inbox_cache_key = "#{cache_key}/unread_inbox_count-#{inbox_updated_at}" end diff --git a/app/models/user/notification_methods.rb b/app/models/user/notification_methods.rb new file mode 100644 index 00000000..41ad8a2e --- /dev/null +++ b/app/models/user/notification_methods.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module User::NotificationMethods + def unread_notification_count + Rails.cache.fetch(notification_cache_key) do + count = Notification.for(self).where(new: true).count(:id) + + # Returning +nil+ here in order to not display a counter + # at all when there aren't any notifications + return nil unless count.positive? + + count + end + end + + def notification_cache_key = "#{cache_key}/unread_notification_count-#{notifications_updated_at}" + def notification_dropdown_cache_key = "#{cache_key}/notification_dropdown-#{notifications_updated_at}" +end diff --git a/app/models/user/push_notification_methods.rb b/app/models/user/push_notification_methods.rb index 01138633..b7b8f5d9 100644 --- a/app/models/user/push_notification_methods.rb +++ b/app/models/user/push_notification_methods.rb @@ -9,11 +9,17 @@ module User::PushNotificationMethods n.app = app n.registration_ids = [s.subscription.symbolize_keys] n.data = { - message: resource.as_push_notification.to_json + message: resource.as_push_notification.merge(notification_data).to_json, } n.save! PushNotificationWorker.perform_async(n.id) end end + + def notification_data = { + data: { + badge: unread_inbox_count, + }, + } end diff --git a/app/models/user/relationship/block.rb b/app/models/user/relationship/block.rb index 674fdb46..01cd25ed 100644 --- a/app/models/user/relationship/block.rb +++ b/app/models/user/relationship/block.rb @@ -21,12 +21,16 @@ class User raise Errors::BlockingSelf if target_user == self unfollow_and_remove(target_user) - create_relationship(active_block_relationships, target_user) + create_relationship(active_block_relationships, target_user).tap do + expire_blocked_user_ids_cache + end end # Unblock an user def unblock(target_user) - destroy_relationship(active_block_relationships, target_user) + destroy_relationship(active_block_relationships, target_user).tap do + expire_blocked_user_ids_cache + end end # Is self blocking target_user? @@ -34,6 +38,16 @@ class User relationship_active?(blocked_users, target_user) end + # Expire the blocked user ids cache + def expire_blocked_user_ids_cache = Rails.cache.delete(cache_key_blocked_user_ids) + + # Cached ids of the blocked users + def blocked_user_ids_cached + Rails.cache.fetch(cache_key_blocked_user_ids, expires_in: 1.hour) do + blocked_user_ids + end + end + private def unfollow_and_remove(target_user) @@ -43,6 +57,8 @@ class User inboxes.joins(:question).where(questions: { user_id: target_user.id, author_is_anonymous: false }).destroy_all ListMember.joins(:list).where(list: { user_id: target_user.id }, user_id: id).destroy_all end + + def cache_key_blocked_user_ids = "#{cache_key}/blocked_user_ids" end end end diff --git a/app/models/user/relationship/mute.rb b/app/models/user/relationship/mute.rb index 08fe286e..c68d68ac 100644 --- a/app/models/user/relationship/mute.rb +++ b/app/models/user/relationship/mute.rb @@ -16,19 +16,41 @@ class User has_many :muted_by_users, through: :passive_mute_relationships, source: :source end + # Mute a user def mute(target_user) raise Errors::MutingSelf if target_user == self - create_relationship(active_mute_relationships, target_user) + create_relationship(active_mute_relationships, target_user).tap do + expire_muted_user_ids_cache + end end + # Unmute an user def unmute(target_user) - destroy_relationship(active_mute_relationships, target_user) + destroy_relationship(active_mute_relationships, target_user).tap do + expire_muted_user_ids_cache + end end + # Is self muting target_user? def muting?(target_user) relationship_active?(muted_users, target_user) end + + # Expires the muted user ids cache + def expire_muted_user_ids_cache = Rails.cache.delete(cache_key_muted_user_ids) + + # Cached ids of the muted users + def muted_user_ids_cached + Rails.cache.fetch(cache_key_muted_user_ids, expires_in: 1.hour) do + muted_user_ids + end + end + + private + + # Cache key for the muted_user_ids + def cache_key_muted_user_ids = "#{cache_key}/muted_user_ids" end end end diff --git a/app/models/user/timeline_methods.rb b/app/models/user/timeline_methods.rb index a0fa8c55..822f9d8f 100644 --- a/app/models/user/timeline_methods.rb +++ b/app/models/user/timeline_methods.rb @@ -8,7 +8,17 @@ module User::TimelineMethods # @return [ActiveRecord::Relation] the user's timeline def timeline Answer - .where("user_id in (?) OR user_id = ?", following_ids, id) + .then do |query| + blocked_and_muted_user_ids = blocked_user_ids_cached + muted_user_ids_cached + next query if blocked_and_muted_user_ids.empty? + + # build a more complex query if we block or mute someone + # otherwise the query ends up as "anon OR (NOT anon AND user_id NOT IN (NULL))" which will only return anonymous questions + query + .joins(:question) + .where("questions.author_is_anonymous OR (NOT questions.author_is_anonymous AND questions.user_id NOT IN (?))", blocked_and_muted_user_ids) + end + .where("answers.user_id in (?) OR answers.user_id = ?", following_ids, id) .order(:created_at) .reverse_order .includes(comments: %i[user smiles], question: { user: :profile }, user: [:profile], smiles: [:user]) diff --git a/app/validators/typoed_email_validator.rb b/app/validators/typoed_email_validator.rb index e529e1fb..717fae97 100644 --- a/app/validators/typoed_email_validator.rb +++ b/app/validators/typoed_email_validator.rb @@ -22,12 +22,14 @@ class TypoedEmailValidator < ActiveModel::EachValidator gmali.com gmaul.com gnail.com + hornail.com hotamil.com hotmai.com hotmaill.com iclooud.com iclould.com icluod.com + maibox.org protonail.com xn--gmail-xk1c.com yahooo.com diff --git a/app/views/actions/_answer.html.haml b/app/views/actions/_answer.html.haml index de81c031..e63e452b 100644 --- a/app/views/actions/_answer.html.haml +++ b/app/views/actions/_answer.html.haml @@ -1,5 +1,5 @@ .dropdown-menu.dropdown-menu-end{ role: :menu } - - if Subscription.is_subscribed(current_user, answer) + - if subscribed_answer_ids&.include?(answer.id) -# fun joke should subscribe? %a.dropdown-item{ href: "#", data: { a_id: answer.id, action: "ab-submarine", torpedo: "no" } } %i.fa.fa-fw.fa-anchor diff --git a/app/views/actions/_share.html.haml b/app/views/actions/_share.html.haml index 85dbdd4e..65daacc2 100644 --- a/app/views/actions/_share.html.haml +++ b/app/views/actions/_share.html.haml @@ -5,5 +5,8 @@ %a.dropdown-item{ href: tumblr_share_url(answer), target: "_blank" } %i.fa.fa-fw.fa-tumblr = t(".tumblr") + %a.dropdown-item{ href: telegram_share_url(answer), target: "_blank" } + %i.fa.fa-fw.fa-telegram + = t(".telegram") %a.dropdown-item{ href: "#", name: "ab-share" } = t(".other") diff --git a/app/views/answer/show.html.haml b/app/views/answer/show.html.haml index 5f84d47d..55f2a90b 100644 --- a/app/views/answer/show.html.haml +++ b/app/views/answer/show.html.haml @@ -1,4 +1,4 @@ - provide(:title, answer_title(@answer)) - provide(:og, answer_opengraph(@answer)) .container-lg.container--main - = render 'answerbox', a: @answer, display_all: @display_all + = render "answerbox", a: @answer, display_all: @display_all, subscribed_answer_ids: @subscribed_answer_ids diff --git a/app/views/answerbox/_actions.html.haml b/app/views/answerbox/_actions.html.haml index a6438cc2..5c152abe 100644 --- a/app/views/answerbox/_actions.html.haml +++ b/app/views/answerbox/_actions.html.haml @@ -1,8 +1,8 @@ -%button.btn.btn-link.answerbox__action{ type: :button, name: "ab-smile", data: { a_id: a.id, action: current_user&.smiled?(a) ? :unsmile : :smile }, disabled: !user_signed_in? } +%button.btn.btn-link.answerbox__action{ type: :button, name: "ab-smile", data: { a_id: a.id, action: current_user&.smiled?(a) ? :unsmile : :smile, selection_hotkey: "s" }, disabled: !user_signed_in? } %i.fa.fa-fw.fa-smile-o %span{ id: "ab-smile-count-#{a.id}" }= a.smiles.count - unless display_all - %button.btn.btn-link.answerbox__action{ type: :button, name: "ab-comments", data: { a_id: a.id, state: :hidden } } + %button.btn.btn-link.answerbox__action{ type: :button, name: "ab-comments", data: { a_id: a.id, state: :hidden, selection_hotkey: "x" } } %i.fa.fa-fw.fa-comments %span{ id: "ab-comment-count-#{a.id}" }= a.comment_count .btn-group @@ -13,4 +13,4 @@ .btn-group %button.btn.btn-default.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } } %span.caret - = render "actions/answer", answer: a + = render "actions/answer", answer: a, subscribed_answer_ids: diff --git a/app/views/answerbox/_comments.html.haml b/app/views/answerbox/_comments.html.haml index ff77e2ee..023990e9 100644 --- a/app/views/answerbox/_comments.html.haml +++ b/app/views/answerbox/_comments.html.haml @@ -25,10 +25,19 @@ %span.caret = render "actions/comment", comment: comment, answer: a - if user_signed_in? - .form-group.has-feedback.comment__input-group.input-group{ + %button.d-none{ name: "ab-open-and-comment", data: { a_id: a.id, selection_hotkey: "c" } } + .comment__compose-wrapper{ name: "ab-comment-new-group", data: { a_id: a.id, controller: "character-count", character_count_max_value: 512 } } - %input.form-control.comment__input{ type: :text, placeholder: t(".placeholder"), name: "ab-comment-new", data: { a_id: a.id, "character-count-target": "input" } } - %span.text-muted.form-control-feedback.comment__character-count{ id: "ab-comment-charcount-#{a.id}", data: { "character-count-target": "counter" } } 512 - %button.btn.btn-primary.d-none{ type: :button, name: "ab-comment-new-submit", data: { a_id: a.id, "character-count-target": "action" } }= t(".action") + .form-group.has-feedback.comment__input-group.input-group + %textarea.form-control.comment__input{ type: :text, placeholder: t(".placeholder"), name: "ab-comment-new", data: { a_id: a.id, "character-count-target": "input" } } + .comment__submit-wrapper + %button.btn.btn-primary{ + type: :button, + name: "ab-comment-new-submit", + title: t(".action"), + data: { a_id: a.id, "character-count-target": "action" } + } + %i.fa.fa-paper-plane-o + %span.text-muted.form-control-feedback.comment__character-count{ id: "ab-comment-charcount-#{a.id}", data: { "character-count-target": "counter" } } 512 diff --git a/app/views/answerbox/_header.html.haml b/app/views/answerbox/_header.html.haml index 5e60c114..30dd9738 100644 --- a/app/views/answerbox/_header.html.haml +++ b/app/views/answerbox/_header.html.haml @@ -11,7 +11,7 @@ = t(".asked_html", user: user_screen_name(a.question.user, context_user: a.user, author_identifier: a.question.author_is_anonymous ? a.question.author_identifier: nil), time: time_tooltip(a.question)) - if !a.question.author_is_anonymous && !a.question.direct Ā· - %a{ href: question_path(a.question.user.screen_name, a.question.id) } + %a{ href: question_path(a.question.user.screen_name, a.question.id), data: { selection_hotkey: "a" } } = t(".answers", count: a.question.answer_count) .answerbox__question-body{ data: { controller: a.question.long? ? "collapse" : nil } } .answerbox__question-text{ class: a.question.long? && !display_all ? "collapsed" : "", data: { collapse_target: "content" } } diff --git a/app/views/application/_answerbox.html.haml b/app/views/application/_answerbox.html.haml index 20b88963..b84a5f17 100644 --- a/app/views/application/_answerbox.html.haml +++ b/app/views/application/_answerbox.html.haml @@ -1,5 +1,5 @@ - display_all ||= nil -.card.answerbox{ data: { id: a.id, q_id: a.question.id } } +.card.answerbox{ data: { id: a.id, q_id: a.question.id, navigation_target: "traversable" } } - if @question.nil? = render "answerbox/header", a: a, display_all: display_all .card-body @@ -19,9 +19,9 @@ %h6.answerbox__answer-user = raw t(".answered", hide: hidespan(t(".hide"), "d-none d-sm-inline"), user: user_screen_name(a.user)) .answerbox__answer-date - = link_to(raw(t("time.distance_ago", time: time_tooltip(a))), answer_path(a.user.screen_name, a.id)) + = link_to(raw(t("time.distance_ago", time: time_tooltip(a))), answer_path(a.user.screen_name, a.id), data: { selection_hotkey: "l" }) .col-md-6.d-flex.d-md-block.answerbox__actions - = render "answerbox/actions", a: a, display_all: display_all + = render "answerbox/actions", a:, display_all:, subscribed_answer_ids: - else .row .col-md-6.text-start.text-muted @@ -33,7 +33,7 @@ %i.fa.fa-thumbtack = t(".pinned") .col-md-6.d-md-flex.answerbox__actions - = render "answerbox/actions", a: a, display_all: display_all + = render "answerbox/actions", a:, display_all:, subscribed_answer_ids: .card-footer{ id: "ab-comments-section-#{a.id}", class: display_all.nil? ? "d-none" : nil } %div{ id: "ab-smiles-#{a.id}" }= render "answerbox/smiles", a: a %div{ id: "ab-comments-#{a.id}" }= render "answerbox/comments", a: a diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index a3d2e34c..df41170a 100644 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -5,7 +5,7 @@ .card.mt-3 .card-body %h1 Resend confirmation instructions - = bootstrap_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }, data: { turbo: false }) do |f| + = bootstrap_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| = render 'devise/shared/error_messages', resource: resource = f.text_field :screen_name, autofocus: true, label: 'User name' diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index e1cdfbed..0106de7a 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -5,7 +5,7 @@ .card.mt-3 .card-body %h1 Change your password - = bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }, data: { turbo: false }) do |f| + = bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, data: { turbo: false } }) do |f| = render 'devise/shared/error_messages', resource: resource = f.hidden_field :reset_password_token diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 13ca7e11..32ce5335 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -5,7 +5,7 @@ .card.mt-3 .card-body %h1 Forgot your password? - = bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }, data: { turbo: false }) do |f| + = bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| = render 'devise/shared/error_messages', resource: resource = f.email_field :email, autofocus: true, label: 'Email address' diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index d0fa29d6..698e63b9 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -5,7 +5,7 @@ .card.mt-3 .card-body %h1= t(".title") - = bootstrap_form_for(resource, as: resource_name, url: registration_path(resource_name), data: { turbo: false }) do |f| + = bootstrap_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| = render "devise/shared/error_messages", resource: resource = render "layouts/messages" diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml index e5f2bf50..d4371be6 100644 --- a/app/views/devise/unlocks/new.html.haml +++ b/app/views/devise/unlocks/new.html.haml @@ -3,7 +3,7 @@ %h1 Resend unlock instructions = render 'layouts/messages' - = bootstrap_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }, data: { turbo: false }) do |f| + = bootstrap_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| = render 'devise/shared/error_messages', resource: resource = f.email_field :email, autofocus: true, label: 'Email address' diff --git a/app/views/discover/index.html.haml b/app/views/discover/index.html.haml index c18bdd71..de792f8a 100644 --- a/app/views/discover/index.html.haml +++ b/app/views/discover/index.html.haml @@ -17,9 +17,9 @@ %li.nav-item{ role: "presentation" } %a.nav-link{ href: "#comments", role: :tab, aria: { controls: "comments" }, data: { bs_toggle: :tab } }= t(".content.tab.comments") .tab-content.mt-3 - = render "discover/tab/answers", answers: @popular_answers + = render "discover/tab/answers", answers: @popular_answers, subscribed_answer_ids: @subscribed_answer_ids = render "discover/tab/questions", questions: @popular_questions - = render "discover/tab/discussed", comments: @most_discussed + = render "discover/tab/discussed", comments: @most_discussed, subscribed_answer_ids: @subscribed_answer_ids .col-md-5.col-sm-6 %h2= t(".people.heading") %p= t(".people.description") diff --git a/app/views/discover/tab/_answers.html.haml b/app/views/discover/tab/_answers.html.haml index 68f0bd79..4438edec 100644 --- a/app/views/discover/tab/_answers.html.haml +++ b/app/views/discover/tab/_answers.html.haml @@ -1,3 +1,3 @@ .tab-pane.active.fade.show{ role: :tabpanel, id: "answers" } - answers.each do |a| - = render "answerbox", a: a + = render "answerbox", a:, subscribed_answer_ids: diff --git a/app/views/discover/tab/_discussed.html.haml b/app/views/discover/tab/_discussed.html.haml index 1a851e68..7b3b7375 100644 --- a/app/views/discover/tab/_discussed.html.haml +++ b/app/views/discover/tab/_discussed.html.haml @@ -1,3 +1,3 @@ .tab-pane.fade{ role: :tabpanel, id: "comments" } - comments.each do |a| - = render "answerbox", a: a + = render "answerbox", a:, subscribed_answer_ids: diff --git a/app/views/inbox/_entry.html.haml b/app/views/inbox/_entry.html.haml index 769d4f10..eb92ed7d 100644 --- a/app/views/inbox/_entry.html.haml +++ b/app/views/inbox/_entry.html.haml @@ -49,6 +49,9 @@ %a.btn.btn-primary{ href: "#", data: { inbox_sharing_target: "tumblr" }, target: "_blank" } %i.fab.fa-tumblr.fa-fw Tumblr + %a.btn.btn-primary{ href: "#", data: { inbox_sharing_target: "telegram" }, target: "_blank" } + %i.fab.fa-telegram.fa-fw + Telegram - if current_user.sharing_custom_url.present? %a.btn.btn-primary{ href: current_user.sharing_custom_url, data: { inbox_sharing_target: "custom" }, target: "_blank" } = current_user.display_sharing_custom_url diff --git a/app/views/inbox/show.html.haml b/app/views/inbox/show.html.haml index 5c5de4e4..802127ee 100644 --- a/app/views/inbox/show.html.haml +++ b/app/views/inbox/show.html.haml @@ -11,4 +11,5 @@ class: "btn btn-light", method: :get, params: { last_id: @inbox_last_id, author: @author }.compact, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/inbox/show.turbo_stream.haml b/app/views/inbox/show.turbo_stream.haml index fffc48fc..bbd233bd 100644 --- a/app/views/inbox/show.turbo_stream.haml +++ b/app/views/inbox/show.turbo_stream.haml @@ -8,4 +8,5 @@ class: "btn btn-light", method: :get, params: { last_id: @inbox_last_id, author: @author }.compact, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/layouts/base.html.haml b/app/views/layouts/base.html.haml index f8464ac6..0607c377 100644 --- a/app/views/layouts/base.html.haml +++ b/app/views/layouts/base.html.haml @@ -18,7 +18,7 @@ %link{ rel: 'icon', href: '/icons/maskable_icon_x192.png', sizes: '192x192' } %link{ rel: 'icon', href: '/images/favicon/favicon-32.png', sizes: '32x32' } %title= yield(:title) - = stylesheet_link_tag 'application', data: { 'turbo-track': 'reload' } + = stylesheet_link_tag "application", data: { 'turbo-track': "reload" }, media: "all" = javascript_include_tag 'application', data: { 'turbo-track': 'reload' }, defer: true = csrf_meta_tags = yield(:og) @@ -31,6 +31,7 @@ = render 'shared/announcements' = yield = render "shared/formatting" + = render "shared/hotkeys" .d-none#toasts - if Rails.env.development? #debug diff --git a/app/views/moderation/inbox/index.html.haml b/app/views/moderation/inbox/index.html.haml index de47e09e..7e400437 100644 --- a/app/views/moderation/inbox/index.html.haml +++ b/app/views/moderation/inbox/index.html.haml @@ -14,4 +14,5 @@ class: "btn btn-light", method: :get, params: { last_id: @inbox_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/moderation/inbox/index.turbo_stream.haml b/app/views/moderation/inbox/index.turbo_stream.haml index 3177f928..6eca17d0 100644 --- a/app/views/moderation/inbox/index.turbo_stream.haml +++ b/app/views/moderation/inbox/index.turbo_stream.haml @@ -8,4 +8,5 @@ class: "btn btn-light", method: :get, params: { last_id: @inbox_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/moderation/reports/index.html.haml b/app/views/moderation/reports/index.html.haml index 9116f8d4..bf92cdf7 100644 --- a/app/views/moderation/reports/index.html.haml +++ b/app/views/moderation/reports/index.html.haml @@ -8,6 +8,7 @@ class: "btn btn-light", method: :get, params: { last_id: @reports_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } - parent_layout "moderation" diff --git a/app/views/moderation/reports/index.turbo_stream.haml b/app/views/moderation/reports/index.turbo_stream.haml index 8499f49b..e379692f 100644 --- a/app/views/moderation/reports/index.turbo_stream.haml +++ b/app/views/moderation/reports/index.turbo_stream.haml @@ -8,4 +8,5 @@ class: "btn btn-light", method: :get, params: { last_id: @reports_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/navigation/_desktop.html.haml b/app/views/navigation/_desktop.html.haml index eb8aba63..05e9a6a3 100644 --- a/app/views/navigation/_desktop.html.haml +++ b/app/views/navigation/_desktop.html.haml @@ -9,28 +9,33 @@ %span.badge.rounded-pill.bg-warning.text-bg-warning.fs-7 DEV %ul.nav.navbar-nav.me-auto - = nav_entry t("navigation.timeline"), root_path, icon: 'home' - = nav_entry t("navigation.inbox"), '/inbox', icon: 'inbox', badge: inbox_count + = nav_entry t("navigation.timeline"), root_path, icon: "home", hotkey: "g t" + = nav_entry t("navigation.inbox"), "/inbox", icon: "inbox", badge: inbox_count, badge_attr: { data: { controller: "pwa-badge" } }, hotkey: "g i" - if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod? - = nav_entry t("navigation.discover"), discover_path, icon: 'compass' + = nav_entry t("navigation.discover"), discover_path, icon: "compass", hotkey: "g d" %ul.nav.navbar-nav - if @user.present? && @user != current_user %li.nav-item.d-none.d-sm-block{ data: { bs_toggle: 'tooltip', bs_placement: 'bottom' }, title: t(".list") } %a.nav-link{ href: '#', data: { bs_target: '#modal-list-memberships', bs_toggle: :modal } } %i.fa.fa-list.hidden-xs %span.d-none.d-sm-inline.d-md-none= t(".list") - = nav_entry t("navigation.notifications"), notifications_path, badge: notification_count, class: 'd-block d-sm-none' + = nav_entry t("navigation.notifications"), notifications_path, class: "d-block d-sm-none", hotkey: "g n" %li.nav-item.dropdown.d-none.d-sm-block %a.nav-link.dropdown-toggle{ href: '#', data: { bs_toggle: :dropdown } } - - if notification_count.nil? - %i.fa.fa-bell-o - - else - %i.fa.fa-bell - %span.visually-hidden= t("navigation.notifications") - %span.badge= notification_count - = render 'navigation/dropdown/notifications', notifications: notifications, size: "desktop" + %turbo-frame#notification-desktop-icon + - if notification_count.nil? + %i.fa.fa-bell-o + - else + %i.fa.fa-bell + %span.visually-hidden= t("navigation.notifications") + %span.badge= notification_count + .dropdown-menu.dropdown-menu-end.notification-dropdown + %turbo-frame#notifications-dropdown-list + - cache current_user.notification_dropdown_cache_key do + - notifications = Notification.for(current_user).where(new: true).includes([:target]).limit(4) + = render "navigation/dropdown/notifications", notifications:, size: "desktop" %li.nav-item.d-none.d-sm-block{ data: { bs_toggle: 'tooltip', bs_placement: 'bottom' }, title: t('.ask_question') } - %a.nav-link{ href: '#', name: 'toggle-all-ask', data: { bs_target: '#modal-ask-followers', bs_toggle: :modal } } + %a.nav-link{ href: "#", name: "toggle-all-ask", data: { bs_target: "#modal-ask-followers", bs_toggle: :modal, hotkey: "n" } } %i.fa.fa-pencil-square-o %li.nav-item.dropdown.profile--image-dropdown %a.nav-link.dropdown-toggle.p-sm-0{ href: "#", data: { bs_toggle: :dropdown } } diff --git a/app/views/navigation/_main.html.haml b/app/views/navigation/_main.html.haml index 3d08fa1e..72c03da0 100644 --- a/app/views/navigation/_main.html.haml +++ b/app/views/navigation/_main.html.haml @@ -1,7 +1,9 @@ -- notifications = Notification.for(current_user).where(new: true).includes([:target]).limit(4) -= render 'navigation/desktop', notifications: notifications -= render 'navigation/mobile', notifications: notifications +:ruby + inbox_count = current_user.unread_inbox_count + notification_count = current_user.unread_notification_count += render "navigation/desktop", inbox_count:, notification_count: += render "navigation/mobile", inbox_count:, notification_count: -= render 'modal/ask' -%button.btn.btn-primary.btn-fab.d-block.d-lg-none{ data: { bs_target: '#modal-ask-followers', bs_toggle: :modal }, type: 'button' } += render "modal/ask" +%button.btn.btn-primary.btn-fab.d-block.d-lg-none.d-print-none{ data: { bs_target: "#modal-ask-followers", bs_toggle: :modal }, type: "button" } %i.fa.fa-pencil-square-o diff --git a/app/views/navigation/_mobile.html.haml b/app/views/navigation/_mobile.html.haml index 37023212..97e37ccc 100644 --- a/app/views/navigation/_mobile.html.haml +++ b/app/views/navigation/_mobile.html.haml @@ -9,7 +9,7 @@ - if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod? = nav_entry t("navigation.discover"), discover_path, icon: 'compass', icon_only: true = nav_entry t("navigation.notifications"), notifications_path("all"), icon: notifications_icon, - badge: notification_count, badge_color: "primary", icon_only: true + badge: notification_count, badge_color: "primary", badge_attr: { id: "notification-mobile-count" }, icon_only: true %li.nav-item.profile--image-dropdown %a.nav-link{ href: '#', data: { bs_toggle: 'dropdown', bs_target: '#rs-mobile-nav-profile' }, aria: { controls: 'rs-mobile-nav-profile', expanded: 'false' } } %img.avatar-md.d-inline{ src: current_user.profile_picture.url(:small) } diff --git a/app/views/navigation/dropdown/_notifications.html.haml b/app/views/navigation/dropdown/_notifications.html.haml index 25dd3e79..94736452 100644 --- a/app/views/navigation/dropdown/_notifications.html.haml +++ b/app/views/navigation/dropdown/_notifications.html.haml @@ -1,16 +1,18 @@ -.dropdown-menu.dropdown-menu-end.notification-dropdown{ id: "rs-#{size}-nav-notifications" } - - if notifications.count.zero? - %a.dropdown-item.text-center{ href: notifications_path('all') } - %i.fa.fa-fw.fa-chevron-right - = t(".all") - .dropdown-item.text-center.p-2 - %i.fa.fa-bell-o.notification__bell-icon - %p= t(".none") - - else - %a.dropdown-item.text-center{ href: notifications_path } - %i.fa.fa-fw.fa-chevron-right - = t(".new") - - notifications.each do |notification| - .dropdown-item - = render "notifications/type/#{notification.target.class.name.downcase.split('::').last}", notification: notification +- if notifications.count.zero? + %a.dropdown-item.text-center{ href: notifications_path('all'), data: { turbo_frame: "_top" } } + %i.fa.fa-fw.fa-chevron-right + = t(".all") + .dropdown-item.text-center.p-2 + %i.fa.fa-bell-o.notification__bell-icon + %p= t(".none") +- else + %a.dropdown-item.text-center{ href: notifications_path, data: { turbo_frame: "_top" } } + %i.fa.fa-fw.fa-chevron-right + = t(".new") + = link_to notifications_read_path, class: "dropdown-item text-center", data: { turbo_stream: true, turbo_method: :post } do + %i.fa.fa-fw.fa-check-double + = t(".mark_as_read") + - notifications.each do |notification| + .dropdown-item + = render "notifications/type/#{notification.target.class.name.downcase.split('::').last}", notification: diff --git a/app/views/navigation/dropdown/_profile.html.haml b/app/views/navigation/dropdown/_profile.html.haml index 611e8662..aef649a9 100644 --- a/app/views/navigation/dropdown/_profile.html.haml +++ b/app/views/navigation/dropdown/_profile.html.haml @@ -1,6 +1,6 @@ .dropdown-menu.dropdown-menu-end.profile-dropdown{ id: "rs-#{size}-nav-profile" } %h6.dropdown-header.d-none.d-sm-block= current_user.screen_name - %a.dropdown-item{ href: user_path(current_user) } + %a.dropdown-item{ href: user_path(current_user), data: { hotkey: "g p" } } %i.fa.fa-fw.fa-user = t(".profile") %a.dropdown-item{ href: edit_user_registration_path } @@ -34,6 +34,10 @@ %i.fa.fa-fw.fa-flask = t(".feedback.features") .dropdown-divider + %a.dropdown-item{ href: "#", data: { bs_target: "#modal-hotkeys", bs_toggle: "modal", hotkey: "Shift+?,?,Shift+Ɵ" } } + %i.fa.fa-keyboard + = t(".hotkeys") + .dropdown-divider = link_to destroy_user_session_path, data: { turbo_method: :delete }, class: "dropdown-item" do %i.fa.fa-fw.fa-sign-out = t("voc.logout") diff --git a/app/views/navigation/notifications.turbo_stream.haml b/app/views/navigation/notifications.turbo_stream.haml new file mode 100644 index 00000000..1ff5be9f --- /dev/null +++ b/app/views/navigation/notifications.turbo_stream.haml @@ -0,0 +1,13 @@ += turbo_stream.update "notifications-dropdown-list" do + = render "navigation/dropdown/notifications", notifications: + += turbo_stream.update "notification-desktop-icon" do + - if notification_count.nil? + %i.fa.fa-bell-o + - else + %i.fa.fa-bell + %span.visually-hidden= t("navigation.notifications") + %span.badge= notification_count + += turbo_stream.update "notification-mobile-count" do + %span.badge.badge-primary= notification_count diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml index 63c65b84..f555ef33 100644 --- a/app/views/notifications/index.html.haml +++ b/app/views/notifications/index.html.haml @@ -22,6 +22,7 @@ class: "btn btn-light", method: :get, params: { last_id: @notifications_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } - provide(:title, generate_title(t(".title"))) diff --git a/app/views/notifications/index.turbo_stream.haml b/app/views/notifications/index.turbo_stream.haml index 5150c813..4339faae 100644 --- a/app/views/notifications/index.turbo_stream.haml +++ b/app/views/notifications/index.turbo_stream.haml @@ -11,4 +11,5 @@ class: "btn btn-light", method: :get, params: { last_id: @notifications_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/notifications/type/_answer.html.haml b/app/views/notifications/type/_answer.html.haml index bef0030a..890fb394 100644 --- a/app/views/notifications/type/_answer.html.haml +++ b/app/views/notifications/type/_answer.html.haml @@ -6,12 +6,12 @@ %img.avatar-xs{ src: notification.target.user.profile_picture.url(:small), loading: :lazy } = t(".heading_html", user: user_screen_name(notification.target.user), - question: link_to(t(".link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.id)), + question: link_to(t(".link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.id), target: "_top"), time: time_tooltip(notification.target)) .list-group .list-group-item %h6.notification__list-heading= t("activerecord.models.question.one") - = markdown notification.target.question.content[0..60] + (notification.target.question.content.length > 60 ? '[...]' : '') + = markdown notification.target.question.content[0..60] + (notification.target.question.content.length > 60 ? "[ā€¦]" : "") .list-group-item %h6.notification__list-heading= t("activerecord.models.answer.one") - = markdown notification.target.content[0..60] + (notification.target.content.length > 60 ? '[...]' : '') + = markdown notification.target.content[0..60] + (notification.target.content.length > 60 ? "[ā€¦]" : "") diff --git a/app/views/notifications/type/_comment.html.haml b/app/views/notifications/type/_comment.html.haml index 26cb55cd..56c3797c 100644 --- a/app/views/notifications/type/_comment.html.haml +++ b/app/views/notifications/type/_comment.html.haml @@ -7,22 +7,32 @@ - if notification.target.answer.user == current_user = t(".heading_html", user: user_screen_name(notification.target.user), - answer: link_to(t(".active.link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.answer.id)), + answer: link_to(t(".active.link_text"), + answer_path(username: notification.target.user.screen_name, + id: notification.target.answer.id), + target: "_top"), time: time_tooltip(notification.target)) - elsif notification.target.user == notification.target.answer.user = t(".heading_html", user: user_screen_name(notification.target.user), - answer: link_to(t(".passive.link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.answer.id)), + answer: link_to(t(".passive.link_text"), + answer_path(username: notification.target.user.screen_name, + id: notification.target.answer.id), + target: "_top"), time: time_tooltip(notification.target)) - else = t(".heading_html", user: user_screen_name(notification.target.user), - answer: link_to(t(".other.link_text_html", user: user_screen_name(notification.target.answer.user, url: false)), answer_path(username: notification.target.user.screen_name, id: notification.target.answer.id)), + answer: link_to(t(".other.link_text_html", + user: user_screen_name(notification.target.answer.user, url: false)), + answer_path(username: notification.target.user.screen_name, + id: notification.target.answer.id), + target: "_top"), time: time_tooltip(notification.target)) .list-group .list-group-item %h6.notification__list-heading= t("activerecord.models.answer.one") - = markdown notification.target.answer.content[0..60] + (notification.target.answer.content.length > 60 ? '[...]' : '') + = markdown notification.target.answer.content[0..60] + (notification.target.answer.content.length > 60 ? "[ā€¦]" : "") .list-group-item %h6.notification__list-heading= t("activerecord.models.comment.one") - = markdown notification.target.content[0..60] + (notification.target.content.length > 60 ? '[...]' : '') + = markdown notification.target.content[0..60] + (notification.target.content.length > 60 ? "[ā€¦]" : "") diff --git a/app/views/notifications/type/_dataexport.html.haml b/app/views/notifications/type/_dataexport.html.haml index 3da53c4c..eeffef10 100644 --- a/app/views/notifications/type/_dataexport.html.haml +++ b/app/views/notifications/type/_dataexport.html.haml @@ -5,4 +5,4 @@ %h6.notification__user = t(".heading") .notification__text - = t(".text_html", settings_export: link_to(t(".settings_export"), settings_export_path)) + = t(".text_html", settings_export: link_to(t(".settings_export"), settings_export_path, target: "_top")) diff --git a/app/views/notifications/type/_follow.html.haml b/app/views/notifications/type/_follow.html.haml index ec44fc56..0994b6de 100644 --- a/app/views/notifications/type/_follow.html.haml +++ b/app/views/notifications/type/_follow.html.haml @@ -5,4 +5,4 @@ %h6.notification__user = user_screen_name notification.target.source .notification__text - = t(".heading_html", time: time_ago_in_words(notification.target.created_at)) + = t(".heading_html", time: time_ago_in_words(notification.target.created_at), target: "_top") diff --git a/app/views/notifications/type/_nilclass.html.haml b/app/views/notifications/type/_nilclass.html.haml new file mode 100644 index 00000000..a6932516 --- /dev/null +++ b/app/views/notifications/type/_nilclass.html.haml @@ -0,0 +1,2 @@ +-# This is a stub to prevent errors on broken notifications on content being asynchronously destroyed. +- logger.error "Notification ##{notification.id} has a target which doesn't exist. (Target: #{notification.target_type}##{notification.target_id})" diff --git a/app/views/notifications/type/_reaction.html.haml b/app/views/notifications/type/_reaction.html.haml index 8462dd88..ae9259c1 100644 --- a/app/views/notifications/type/_reaction.html.haml +++ b/app/views/notifications/type/_reaction.html.haml @@ -4,17 +4,23 @@ .flex-grow-1 .notification__heading %img.avatar-xs{ src: notification.target.user.profile_picture.url(:small), loading: :lazy } - - if notification.target.parent_type == 'Answer' + - if notification.target.parent_type == "Answer" = t(".heading_html", user: user_screen_name(notification.target.user), - type: link_to(t(".#{notification.target.parent_type.downcase}.link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.parent.id)), + type: link_to(t(".#{notification.target.parent_type.downcase}.link_text"), + answer_path(username: notification.target.user.screen_name, + id: notification.target.parent.id), + target: "_top"), time: time_tooltip(notification.target)) - - elsif notification.target.parent_type == 'Comment' + - elsif notification.target.parent_type == "Comment" = t(".heading_html", user: user_screen_name(notification.target.user), - type: link_to(t(".#{notification.target.parent_type.downcase}.link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.parent.answer.id)), + type: link_to(t(".#{notification.target.parent_type.downcase}.link_text"), + answer_path(username: notification.target.user.screen_name, + id: notification.target.parent.answer.id), + target: "_top"), time: time_tooltip(notification.target)) .list-group .list-group-item %h6.notification__list-heading= t("activerecord.models.#{notification.target.parent_type.downcase}.one") - = markdown notification.target.parent.content[0..60] + (notification.target.parent.content.length > 60 ? '[...]' : '') + = markdown notification.target.parent.content[0..60] + (notification.target.parent.content.length > 60 ? "[ā€¦]" : "") diff --git a/app/views/notifications/type/_webpushsubscription.html.haml b/app/views/notifications/type/_webpushsubscription.html.haml index b077f489..599130ce 100644 --- a/app/views/notifications/type/_webpushsubscription.html.haml +++ b/app/views/notifications/type/_webpushsubscription.html.haml @@ -7,4 +7,4 @@ %h6.notification__user = t(".heading") .notification__text - = t(".text_html", settings_push: link_to(t(".settings_push"), settings_push_notifications_path)) + = t(".text_html", settings_push: link_to(t(".settings_push"), settings_push_notifications_path, target: "_top")) diff --git a/app/views/question/show.html.haml b/app/views/question/show.html.haml index 57137240..e1d0b238 100644 --- a/app/views/question/show.html.haml +++ b/app/views/question/show.html.haml @@ -2,9 +2,11 @@ = render "question", question: @question, hidden: false = render "question", question: @question, hidden: true .container.question-page - #answers + #answers{ data: { controller: "navigation" } } + %button.d-none{ data: { hotkey: "j", action: "navigation#down" } } + %button.d-none{ data: { hotkey: "k", action: "navigation#up" } } - @answers.each do |a| - = render "answerbox", a: a, show_question: false + = render "answerbox", a:, show_question: false, subscribed_answer_ids: @subscribed_answer_ids - if @more_data_available .d-flex.justify-content-center.justify-content-sm-start#paginator @@ -12,6 +14,7 @@ class: "btn btn-light", method: :get, params: { last_id: @answers_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } - if user_signed_in? && !current_user.answered?(@question) && current_user != @question.user && @question.user&.privacy_allow_stranger_answers diff --git a/app/views/question/show.turbo_stream.haml b/app/views/question/show.turbo_stream.haml index 9ef4eb64..96facf13 100644 --- a/app/views/question/show.turbo_stream.haml +++ b/app/views/question/show.turbo_stream.haml @@ -1,6 +1,6 @@ = turbo_stream.append "answers" do - @answers.each do |a| - = render "answerbox", a: a, show_question: false + = render "answerbox", a:, show_question: false, subscribed_answer_ids: @subscribed_answer_ids = turbo_stream.update "paginator" do - if @more_data_available @@ -8,4 +8,5 @@ class: "btn btn-light", method: :get, params: { last_id: @answers_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/settings/two_factor_authentication/otp_authentication/recovery_keys.html.haml b/app/views/settings/two_factor_authentication/otp_authentication/recovery_keys.html.haml index d2823926..692d9296 100644 --- a/app/views/settings/two_factor_authentication/otp_authentication/recovery_keys.html.haml +++ b/app/views/settings/two_factor_authentication/otp_authentication/recovery_keys.html.haml @@ -4,7 +4,7 @@ .card .card-body .d-none.d-print-block.totp-setup__recovery-icon - %i.fa.fa-comments + %img{ src: "/icons/icon.svg", width: 96 } %h1.totp-setup__recovery-title= t(".heading", app_name: APP_CONFIG['site_name']) %ul.totp-setup__recovery-codes diff --git a/app/views/shared/_hotkeys.html.haml b/app/views/shared/_hotkeys.html.haml new file mode 100644 index 00000000..e62c008b --- /dev/null +++ b/app/views/shared/_hotkeys.html.haml @@ -0,0 +1,80 @@ +.modal.fade#modal-hotkeys{ aria: { hidden: true, labelledby: "modal-hotkeys-label" }, role: :dialog, tabindex: -1 } + .modal-dialog + .modal-content + .modal-header + %h5.modal-title#modal-hotkeys-label= t(".title") + %button.btn-close{ data: { bs_dismiss: :modal }, type: :button } + %span.visually-hidden= t("voc.close") + .modal-body + .row + .col-6 + .card + .card-header + %h5.card-title= t(".navigation.title") + %ul.list-group.list-group-flush + %li.list-group-item + = t("navigation.timeline") + %kbd + %kbd g + %kbd t + %li.list-group-item + = t("navigation.inbox") + %kbd + %kbd g + %kbd i + - if APP_CONFIG.dig(:features, :discover, :enabled) || current_user&.mod? + %li.list-group-item + = t("navigation.discover") + %kbd + %kbd g + %kbd d + %li.list-group-item + = t("navigation.notifications") + %kbd + %kbd g + %kbd n + %li.list-group-item + = t("navigation.dropdown.profile.profile") + %kbd + %kbd g + %kbd p + .col-6 + .card + .card-header + %h5.card-title= t(".global.title") + %ul.list-group.list-group-flush + %li.list-group-item + = t(".global.navigation.up") + %kbd k + %li.list-group-item + = t(".global.navigation.down") + %kbd j + %li.list-group-item + = t("navigation.desktop.ask_question") + %kbd n + %li.list-group-item + = t("voc.load") + %kbd . + %li.list-group-item + = t(".show_dialog") + %kbd ? + .card + .card-header + %h5.card-title= t(".answer.title") + %ul.list-group.list-group-flush + %li.list-group-item + = t("voc.smile") + %kbd s + %li.list-group-item + = t(".answer.view_comments") + %kbd x + %li.list-group-item + = t(".answer.comment") + %kbd c + %li.list-group-item + = t(".answer.all_answers") + %kbd a + %li.list-group-item + = t(".answer.view_answer") + %kbd l + diff --git a/app/views/timeline/timeline.html.haml b/app/views/timeline/timeline.html.haml index 06610059..4ed197a8 100644 --- a/app/views/timeline/timeline.html.haml +++ b/app/views/timeline/timeline.html.haml @@ -1,6 +1,8 @@ -#timeline +#timeline{ data: { controller: "navigation" } } + %button.d-none{ data: { hotkey: "j", action: "navigation#down" } } + %button.d-none{ data: { hotkey: "k", action: "navigation#up" } } - @timeline.each do |answer| - = render "answerbox", a: answer + = render "answerbox", a: answer, subscribed_answer_ids: @subscribed_answer_ids - if @more_data_available .d-flex.justify-content-center#paginator @@ -8,6 +10,7 @@ class: "btn btn-light", method: :get, params: { last_id: @timeline_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } - provide(:title, @title || APP_CONFIG["site_name"]) diff --git a/app/views/timeline/timeline.turbo_stream.haml b/app/views/timeline/timeline.turbo_stream.haml index 525cbba9..1802f630 100644 --- a/app/views/timeline/timeline.turbo_stream.haml +++ b/app/views/timeline/timeline.turbo_stream.haml @@ -1,6 +1,6 @@ = turbo_stream.append "timeline" do - @timeline.each do |answer| - = render "answerbox", a: answer + = render "answerbox", a: answer, subscribed_answer_ids: @subscribed_answer_ids = turbo_stream.update "paginator" do - if @more_data_available @@ -8,4 +8,5 @@ class: "btn btn-light", method: :get, params: { last_id: @timeline_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/user/questions.html.haml b/app/views/user/questions.html.haml index ba589f59..aa410ffd 100644 --- a/app/views/user/questions.html.haml +++ b/app/views/user/questions.html.haml @@ -8,6 +8,7 @@ class: "btn btn-light", method: :get, params: { last_id: @questions_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } - provide(:title, questions_title(@user)) diff --git a/app/views/user/questions.turbo_stream.haml b/app/views/user/questions.turbo_stream.haml index a48fafe9..6fd71f55 100644 --- a/app/views/user/questions.turbo_stream.haml +++ b/app/views/user/questions.turbo_stream.haml @@ -8,4 +8,5 @@ class: "btn btn-light", method: :get, params: { last_id: @questions_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/user/show.html.haml b/app/views/user/show.html.haml index 25240a33..e110ca20 100644 --- a/app/views/user/show.html.haml +++ b/app/views/user/show.html.haml @@ -1,11 +1,14 @@ - unless @user.banned? - #pinned-answers - - @pinned_answers.each do |a| - = render "answerbox", a: + %div{ data: { controller: "navigation" } } + %button.d-none{ data: { hotkey: "j", action: "navigation#down" } } + %button.d-none{ data: { hotkey: "k", action: "navigation#up" } } + #pinned-answers + - @pinned_answers.each do |a| + = render "answerbox", a:, subscribed_answer_ids: @subscribed_answer_ids - #answers - - @answers.each do |a| - = render "answerbox", a: + #answers + - @answers.each do |a| + = render "answerbox", a:, subscribed_answer_ids: @subscribed_answer_ids - if @more_data_available .d-flex.justify-content-center.justify-content-sm-start#paginator @@ -13,6 +16,7 @@ class: "btn btn-light", method: :get, params: { last_id: @answers_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } :ruby diff --git a/app/views/user/show.turbo_stream.haml b/app/views/user/show.turbo_stream.haml index 4b6ce125..d86873da 100644 --- a/app/views/user/show.turbo_stream.haml +++ b/app/views/user/show.turbo_stream.haml @@ -1,6 +1,6 @@ = turbo_stream.append "answers" do - @answers.each do |a| - = render 'answerbox', a: a + = render "answerbox", a:, subscribed_answer_ids: @subscribed_answer_ids = turbo_stream.update "paginator" do - if @more_data_available @@ -8,4 +8,5 @@ class: "btn btn-light", method: :get, params: { last_id: @answers_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/app/views/user/show_follow.html.haml b/app/views/user/show_follow.html.haml index 699df89a..ca284ab8 100644 --- a/app/views/user/show_follow.html.haml +++ b/app/views/user/show_follow.html.haml @@ -9,6 +9,7 @@ class: "btn btn-light", method: :get, params: { last_id: @relationships_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } - provide(:title, t(".title.#{type}", user: @user.profile.safe_name)) diff --git a/app/views/user/show_follow.turbo_stream.haml b/app/views/user/show_follow.turbo_stream.haml index fe434f69..a7c3b180 100644 --- a/app/views/user/show_follow.turbo_stream.haml +++ b/app/views/user/show_follow.turbo_stream.haml @@ -9,4 +9,5 @@ class: "btn btn-light", method: :get, params: { last_id: @relationships_last_id }, + data: { controller: :hotkey, hotkey: "." }, form: { data: { turbo_stream: true } } diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 1345aea6..947195a4 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -253,4 +253,19 @@ Devise.setup do |config| # When using omniauth, Devise cannot automatically set Omniauth path, # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found respectively`, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true end diff --git a/config/locales/controllers.en.yml b/config/locales/controllers.en.yml index ff41c0c6..b0347cc9 100644 --- a/config/locales/controllers.en.yml +++ b/config/locales/controllers.en.yml @@ -202,10 +202,10 @@ en: banned: "I'm sorry, %{name}, I'm afraid I can't do that." reason: "Ban reason: %{reason}" until: "Banned until: %{time}" - info: - one: "You have only one recovery code remaining. Please regenerate your recovery codes from the security settings to avoid being locked out!" - other: "You have %{count} recovery codes remaining." - error: :errors.invalid_otp + info: + one: "You have only one recovery code remaining. Please regenerate your recovery codes from the security settings to avoid being locked out!" + other: "You have %{count} recovery codes remaining." + error: :errors.invalid_otp registrations: destroy: export_pending: "You may not delete your account while account data is currently being exported." diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index d16c9ce5..20994bce 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -81,6 +81,7 @@ en: share: twitter: "Share on Twitter" tumblr: "Share on Tumblr" + telegram: "Share on Telegram" other: "Share on other apps..." admin: announcement: @@ -119,6 +120,7 @@ en: comments: none: "There are no comments yet." placeholder: "Comment..." + action: "Post comment" smiles: none: "No one smiled this yet." application: @@ -316,6 +318,7 @@ en: none: :notifications.index.none all: "Show all notifications" new: "Show all new notifications" + mark_as_read: "Mark all as read" profile: profile: "Show profile" settings: "Settings" @@ -328,6 +331,7 @@ en: heading: "Feedback" bugs: "Bugs" features: "Feature Requests" + hotkeys: "Keyboard Shortcuts" desktop: ask_question: "Ask a question" list: :user.actions.list @@ -581,6 +585,22 @@ en: question: visible_to_you: "Only visible to you as it was asked directly" visible_mod_mode: "You can see this because you are in moderation view" + hotkeys: + navigation: + title: "Navigation" + title: "Keyboard Shortcuts" + show_dialog: "Show this dialog" + answer: + title: "Answer actions" + view_comments: "View comments" + comment: "Write a comment" + all_answers: "View all answers" + view_answer: "View answer page" + global: + navigation: + up: "Move selection up" + down: "Move selection down" + title: "Site-wide" tabs: admin: announcements: "Announcements" diff --git a/config/locales/voc.en.yml b/config/locales/voc.en.yml index 7f674a7e..a05407eb 100644 --- a/config/locales/voc.en.yml +++ b/config/locales/voc.en.yml @@ -17,6 +17,7 @@ en: mute: "Mute" save: "Save changes" show_anonymous_questions: "Show all questions from this user" + smile: "Smile" subscribe: "Subscribe" unsubscribe: "Unsubscribe" register: "Sign up" diff --git a/config/routes.rb b/config/routes.rb index 8f485cb7..3af4b3a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -143,6 +143,7 @@ Rails.application.routes.draw do get "/list/:list_name", to: "timeline#list", as: :list_timeline get "/notifications(/:type)", to: "notifications#index", as: :notifications, defaults: { type: "new" } + post "/notifications", to: "notifications#read", as: :notifications_read post "/inbox/create", to: "inbox#create", as: :inbox_create get "/inbox", to: "inbox#show", as: :inbox diff --git a/db/migrate/20230225143633_add_notification_and_inbox_timestamps_to_users.rb b/db/migrate/20230225143633_add_notification_and_inbox_timestamps_to_users.rb new file mode 100644 index 00000000..953a9207 --- /dev/null +++ b/db/migrate/20230225143633_add_notification_and_inbox_timestamps_to_users.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddNotificationAndInboxTimestampsToUsers < ActiveRecord::Migration[6.1] + def change + change_table :users, bulk: true do |t| + t.timestamp :notifications_updated_at + t.timestamp :inbox_updated_at + end + end +end diff --git a/db/migrate/20230227174822_remove_is_active_from_subscriptions.rb b/db/migrate/20230227174822_remove_is_active_from_subscriptions.rb new file mode 100644 index 00000000..3b8cb4b5 --- /dev/null +++ b/db/migrate/20230227174822_remove_is_active_from_subscriptions.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class RemoveIsActiveFromSubscriptions < ActiveRecord::Migration[6.1] + def up + execute "DELETE FROM subscriptions WHERE is_active = FALSE" + remove_column :subscriptions, :is_active + end +end diff --git a/db/migrate/20230526181715_backfill_missing_created_at_on_commented_notifications.rb b/db/migrate/20230526181715_backfill_missing_created_at_on_commented_notifications.rb new file mode 100644 index 00000000..906b6859 --- /dev/null +++ b/db/migrate/20230526181715_backfill_missing_created_at_on_commented_notifications.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class BackfillMissingCreatedAtOnCommentedNotifications < ActiveRecord::Migration[6.1] + def up + execute <<~SQUIRREL + UPDATE notifications + SET created_at = comments.created_at, updated_at = comments.created_at + FROM comments + WHERE notifications.target_id = comments.id AND notifications.created_at IS NULL AND notifications.type = 'Notification::Commented' + SQUIRREL + + # clean up notifications for deleted comments + Notification::Commented.where(created_at: nil).destroy_all + end + + def down; end +end diff --git a/db/schema.rb b/db/schema.rb index 0e8fadfd..c052e79d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_02_18_142952) do +ActiveRecord::Schema.define(version: 2023_05_26_181715) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -261,7 +261,6 @@ ActiveRecord::Schema.define(version: 2023_02_18_142952) do t.bigint "answer_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.boolean "is_active", default: true t.index ["user_id", "answer_id"], name: "index_subscriptions_on_user_id_and_answer_id" end @@ -364,6 +363,8 @@ ActiveRecord::Schema.define(version: 2023_02_18_142952) do t.boolean "sharing_enabled", default: false t.boolean "sharing_autoclose", default: false t.string "sharing_custom_url" + t.datetime "notifications_updated_at" + t.datetime "inbox_updated_at" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true diff --git a/lib/retrospring/metrics.rb b/lib/retrospring/metrics.rb index 7c52dcb6..9c5ea3bb 100644 --- a/lib/retrospring/metrics.rb +++ b/lib/retrospring/metrics.rb @@ -24,55 +24,65 @@ module Retrospring labels: [:version], preset_labels: { version: Retrospring::Version.to_s, - } + }, ).tap { _1.set 1 } QUESTIONS_ASKED = counter( :retrospring_questions_asked_total, docstring: "How many questions got asked", - labels: %i[anonymous followers generated] + labels: %i[anonymous followers generated], ) QUESTIONS_ANSWERED = counter( :retrospring_questions_answered_total, - docstring: "How many questions got answered" + docstring: "How many questions got answered", ) COMMENTS_CREATED = counter( :retrospring_comments_created_total, - docstring: "How many comments got created" + docstring: "How many comments got created", + ) + + USERS_CREATED = counter( + :retrospring_users_created_total, + docstring: "How many users got created", + ) + + USERS_DESTROYED = counter( + :retrospring_users_destroyed_total, + docstring: "How many users deleted their accounts", ) # metrics from Sidekiq::Stats.new SIDEKIQ = { processed: gauge( :sidekiq_processed, - docstring: "Number of jobs processed by Sidekiq" + docstring: "Number of jobs processed by Sidekiq", ), failed: gauge( :sidekiq_failed, - docstring: "Number of jobs that failed" + docstring: "Number of jobs that failed", ), scheduled_size: gauge( :sidekiq_scheduled_jobs, - docstring: "Number of jobs that are enqueued" + docstring: "Number of jobs that are enqueued", ), retry_size: gauge( :sidekiq_retried_jobs, - docstring: "Number of jobs that are being retried" + docstring: "Number of jobs that are being retried", ), dead_size: gauge( :sidekiq_dead_jobs, - docstring: "Number of jobs that are dead" + docstring: "Number of jobs that are dead", ), processes_size: gauge( :sidekiq_processes, - docstring: "Number of active Sidekiq processes" + docstring: "Number of active Sidekiq processes", ), queue_enqueued: gauge( :sidekiq_queues_enqueued, docstring: "Number of enqueued jobs per queue", - labels: %i[queue] + labels: %i[queue], ), }.freeze end diff --git a/lib/retrospring/version.rb b/lib/retrospring/version.rb index 78573924..72d6e3fa 100644 --- a/lib/retrospring/version.rb +++ b/lib/retrospring/version.rb @@ -15,11 +15,11 @@ module Retrospring def year = 2023 - def month = 2 + def month = 7 - def day = 19 + def day = 7 - def patch = 2 + def patch = 0 def suffix = "" diff --git a/lib/use_case/data_export/user.rb b/lib/use_case/data_export/user.rb index 4f7f8445..2bd4ceb1 100644 --- a/lib/use_case/data_export/user.rb +++ b/lib/use_case/data_export/user.rb @@ -13,6 +13,8 @@ module UseCase otp_secret_key reset_password_sent_at reset_password_token + inbox_updated_at + notifications_updated_at ].freeze IGNORED_FIELDS_PROFILES = %i[ diff --git a/package.json b/package.json index bca4b7f0..34346ae7 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ }, "dependencies": { "@fontsource/lexend": "^4.5.15", - "@fortawesome/fontawesome-free": "^6.3.0", + "@fortawesome/fontawesome-free": "^6.4.0", + "@github/hotkey": "^2.0.1", "@hotwired/stimulus": "^3.2.1", - "@hotwired/turbo-rails": "^7.2.5", - "@melloware/coloris": "^0.17.1", + "@hotwired/turbo-rails": "^7.3.0", + "@melloware/coloris": "^0.21.0", "@popperjs/core": "^2.11", "@rails/request.js": "^0.0.8", "bootstrap": "^5.2", @@ -20,19 +21,19 @@ "croppr": "^2.3.1", "i18n-js": "^4.0", "js-cookie": "2.2.1", - "sass": "^1.58.0", + "sass": "^1.64.1", "sweetalert": "1.1.3", "toastify-js": "^1.12.0", - "typescript": "^4.9.5" + "typescript": "^5.1.6" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^4.11.0", "@typescript-eslint/parser": "^4.11.0", - "esbuild": "^0.17.8", + "esbuild": "^0.18.11", "eslint": "^7.16.0", - "eslint-plugin-import": "^2.27.5", - "stylelint": "^14.16.1", - "stylelint-config-standard-scss": "^6.1.0", - "stylelint-scss": "^4.4.0" + "eslint-plugin-import": "^2.28.0", + "stylelint": "^15.10.2", + "stylelint-config-standard-scss": "^10.0.0", + "stylelint-scss": "^5.0.1" } } diff --git a/public/icons/maskable_icon_x1024.webp b/public/icons/maskable_icon_x1024.webp index e69de29b..15f297d8 100644 Binary files a/public/icons/maskable_icon_x1024.webp and b/public/icons/maskable_icon_x1024.webp differ diff --git a/public/icons/maskable_icon_x128.webp b/public/icons/maskable_icon_x128.webp index e69de29b..0e662cd9 100644 Binary files a/public/icons/maskable_icon_x128.webp and b/public/icons/maskable_icon_x128.webp differ diff --git a/public/icons/maskable_icon_x144.webp b/public/icons/maskable_icon_x144.webp index bc19121f..f8c13815 100644 Binary files a/public/icons/maskable_icon_x144.webp and b/public/icons/maskable_icon_x144.webp differ diff --git a/public/icons/maskable_icon_x192.webp b/public/icons/maskable_icon_x192.webp index e69de29b..7cda2873 100644 Binary files a/public/icons/maskable_icon_x192.webp and b/public/icons/maskable_icon_x192.webp differ diff --git a/public/icons/maskable_icon_x384.webp b/public/icons/maskable_icon_x384.webp index e69de29b..ea6a9f15 100644 Binary files a/public/icons/maskable_icon_x384.webp and b/public/icons/maskable_icon_x384.webp differ diff --git a/public/icons/maskable_icon_x48.webp b/public/icons/maskable_icon_x48.webp index e69de29b..f8b1c163 100644 Binary files a/public/icons/maskable_icon_x48.webp and b/public/icons/maskable_icon_x48.webp differ diff --git a/public/icons/maskable_icon_x512.webp b/public/icons/maskable_icon_x512.webp index e69de29b..a90e2bcf 100644 Binary files a/public/icons/maskable_icon_x512.webp and b/public/icons/maskable_icon_x512.webp differ diff --git a/public/icons/maskable_icon_x72.webp b/public/icons/maskable_icon_x72.webp index e69de29b..5c39dcd0 100644 Binary files a/public/icons/maskable_icon_x72.webp and b/public/icons/maskable_icon_x72.webp differ diff --git a/public/icons/maskable_icon_x96.webp b/public/icons/maskable_icon_x96.webp index e69de29b..34396111 100644 Binary files a/public/icons/maskable_icon_x96.webp and b/public/icons/maskable_icon_x96.webp differ diff --git a/public/service_worker.js b/public/service_worker.js index 8662f085..3e88521c 100644 --- a/public/service_worker.js +++ b/public/service_worker.js @@ -11,27 +11,22 @@ const OFFLINE_CACHE_PATHS = [ self.addEventListener('push', function (event) { if (event.data) { - const notification = event.data.json(); + const contents = event.data.json(); + navigator.setAppBadge(contents.data.badge); - event.waitUntil(self.registration.showNotification(notification.title, { - body: notification.body, - tag: notification.type, - icon: notification.icon, - })); + event.waitUntil(self.registration.showNotification(contents.title, contents)); } else { console.error("Push event received, but it didn't contain any data.", event); } }); self.addEventListener('notificationclick', async event => { - if (event.notification.tag === 'inbox') { + if ("click_url" in event.notification.data) { event.preventDefault(); - return clients.openWindow("/inbox", "_blank").then(result => { + return clients.openWindow(event.notification.data.click_url, "_blank").then(result => { event.notification.close(); return result; }); - } else { - console.warn(`Unhandled notification tag: ${event.notification.tag}`); } }); diff --git a/spec/controllers/ajax/answer_controller_spec.rb b/spec/controllers/ajax/answer_controller_spec.rb index 93374bb5..18c65354 100644 --- a/spec/controllers/ajax/answer_controller_spec.rb +++ b/spec/controllers/ajax/answer_controller_spec.rb @@ -13,7 +13,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do id:, answer:, share: shared_services&.to_json, - inbox: + inbox:, }.compact end @@ -42,7 +42,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do "success" => false, # caught by rescue_from, so status is not peter_dinklage "status" => "parameter_error", - "message" => anything + "message" => anything, } end @@ -70,13 +70,33 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => true, "status" => "okay", - "message" => anything + "message" => anything, } end include_examples "creates the answer" it_behaves_like "fails when answer content is empty" + + context "when the user has sharing enabled" do + before do + user.sharing_enabled = true + user.save + end + + let(:expected_response) do + super().merge( + "sharing" => { + "twitter" => a_string_matching("https://twitter.com/"), + "tumblr" => a_string_matching("https://www.tumblr.com/"), + "telegram" => a_string_matching("https://t.me/"), + "custom" => a_string_matching(/Werfen\+Sie\+nicht\+l%C3%A4nger\+das\+Fenster\+zum\+Geld\+hinaus%21/), + } + ) + end + + include_examples "creates the answer" + end end context "when the inbox entry does not belong to the user" do @@ -85,7 +105,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => false, "status" => "fail", - "message" => anything + "message" => anything, } end @@ -100,7 +120,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => true, "status" => "okay", - "message" => anything + "message" => anything, } end @@ -129,13 +149,33 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do "success" => true, "status" => "okay", "message" => anything, - "render" => anything + "render" => anything, } end include_examples "creates the answer" it_behaves_like "fails when answer content is empty" + + context "when the user has sharing enabled" do + before do + user.sharing_enabled = true + user.save + end + + let(:expected_response) do + super().merge( + "sharing" => { + "twitter" => a_string_matching("https://twitter.com/"), + "tumblr" => a_string_matching("https://www.tumblr.com/"), + "telegram" => a_string_matching("https://t.me/"), + "custom" => a_string_matching(/Werfen\+Sie\+nicht\+l%C3%A4nger\+das\+Fenster\+zum\+Geld\+hinaus%21/), + } + ) + end + + include_examples "creates the answer" + end end context "when question asker does not allow strangers to answer (i.e. question was not in inbox)" do @@ -144,7 +184,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => false, "status" => "privacy_stronk", - "message" => anything + "message" => anything, } end @@ -160,7 +200,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => false, "status" => "answering_other_blocked_self", - "message" => I18n.t("errors.answering_other_blocked_self") + "message" => I18n.t("errors.answering_other_blocked_self"), } end @@ -176,7 +216,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => false, "status" => "answering_self_blocked_other", - "message" => I18n.t("errors.answering_self_blocked_other") + "message" => I18n.t("errors.answering_self_blocked_other"), } end @@ -196,7 +236,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do "success" => false, # caught by rescue_from, so status is not peter_dinklage "status" => "parameter_error", - "message" => anything + "message" => anything, } end @@ -214,7 +254,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => false, "status" => "err", - "message" => anything + "message" => anything, } end @@ -230,7 +270,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do let(:params) do { - answer: answer_id + answer: answer_id, } end @@ -242,7 +282,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => true, "status" => "okay", - "message" => anything + "message" => anything, } end @@ -259,7 +299,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => false, "status" => "nopriv", - "message" => anything + "message" => anything, } end @@ -311,7 +351,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => false, "status" => anything, - "message" => anything + "message" => anything, } end @@ -324,7 +364,7 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do { "success" => false, "status" => "nopriv", - "message" => anything + "message" => anything, } end diff --git a/spec/controllers/ajax/subscription_controller_spec.rb b/spec/controllers/ajax/subscription_controller_spec.rb index 6a14aeee..23499211 100644 --- a/spec/controllers/ajax/subscription_controller_spec.rb +++ b/spec/controllers/ajax/subscription_controller_spec.rb @@ -33,7 +33,7 @@ describe Ajax::SubscriptionController, :ajax_controller, type: :controller do context "when subscription does not exist" do it "creates a subscription on the answer" do expect { subject }.to(change { answer.subscriptions.count }.by(1)) - expect(answer.subscriptions.where(is_active: true).map { |s| s.user.id }.sort).to eq([answer_user.id, user.id].sort) + expect(answer.subscriptions.map { |s| s.user.id }.sort).to eq([answer_user.id, user.id].sort) end include_examples "returns the expected response" @@ -44,7 +44,7 @@ describe Ajax::SubscriptionController, :ajax_controller, type: :controller do it "does not modify the answer's subscriptions" do expect { subject }.to(change { answer.subscriptions.count }.by(0)) - expect(answer.subscriptions.where(is_active: true).map { |s| s.user.id }.sort).to eq([answer_user.id, user.id].sort) + expect(answer.subscriptions.map { |s| s.user.id }.sort).to eq([answer_user.id, user.id].sort) end include_examples "returns the expected response" @@ -105,8 +105,8 @@ describe Ajax::SubscriptionController, :ajax_controller, type: :controller do before(:each) { Subscription.subscribe(user, answer) } it "removes an active subscription from the answer" do - expect { subject }.to(change { answer.subscriptions.where(is_active: true).count }.by(-1)) - expect(answer.subscriptions.where(is_active: true).map { |s| s.user.id }.sort).to eq([answer_user.id].sort) + expect { subject }.to(change { answer.subscriptions.count }.by(-1)) + expect(answer.subscriptions.map { |s| s.user.id }.sort).to eq([answer_user.id].sort) end include_examples "returns the expected response" @@ -123,7 +123,7 @@ describe Ajax::SubscriptionController, :ajax_controller, type: :controller do it "does not modify the answer's subscriptions" do expect { subject }.to(change { answer.subscriptions.count }.by(0)) - expect(answer.subscriptions.where(is_active: true).map { |s| s.user.id }.sort).to eq([answer_user.id].sort) + expect(answer.subscriptions.map { |s| s.user.id }.sort).to eq([answer_user.id].sort) end include_examples "returns the expected response" diff --git a/spec/controllers/inbox_controller_spec.rb b/spec/controllers/inbox_controller_spec.rb index f2ec2c9f..94a8accf 100644 --- a/spec/controllers/inbox_controller_spec.rb +++ b/spec/controllers/inbox_controller_spec.rb @@ -3,7 +3,10 @@ require "rails_helper" describe InboxController, type: :controller do - let(:user) { FactoryBot.create(:user) } + include ActiveSupport::Testing::TimeHelpers + + let(:original_inbox_updated_at) { 1.day.ago } + let(:user) { FactoryBot.create(:user, inbox_updated_at: original_inbox_updated_at) } describe "#show" do shared_examples_for "sets the expected ivars" do @@ -53,7 +56,7 @@ describe InboxController, type: :controller do more_data_available: false, inbox_count: 1, delete_id: "ib-delete-all", - disabled: nil + disabled: nil, } end end @@ -62,6 +65,13 @@ describe InboxController, type: :controller do expect { subject }.to change { inbox_entry.reload.new? }.from(true).to(false) end + it "updates the the timestamp used for caching" do + user.update(inbox_updated_at: original_inbox_updated_at) + travel 1.second do + expect { subject }.to change { user.reload.inbox_updated_at.floor }.from(original_inbox_updated_at.floor).to(Time.now.utc.floor) + end + end + context "when requested the turbo stream format" do subject { get :show, format: :turbo_stream } diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb index bcae9a8d..4eea29cf 100644 --- a/spec/controllers/notifications_controller_spec.rb +++ b/spec/controllers/notifications_controller_spec.rb @@ -3,39 +3,72 @@ require "rails_helper" describe NotificationsController do - subject { get :index, params: { type: :new } } + include ActiveSupport::Testing::TimeHelpers - let(:user) { FactoryBot.create(:user) } + describe "#index" do + subject { get :index, params: { type: :new } } - before do - sign_in(user) - end + let(:original_notifications_updated_at) { 1.day.ago } + let(:user) { FactoryBot.create(:user) } - context "user has no notifications" do - it "should show an empty list" do - subject - expect(response).to render_template(:index) + before do + sign_in(user) + end - expect(controller.instance_variable_get(:@notifications)).to be_empty + context "user has no notifications" do + it "should show an empty list" do + subject + expect(response).to render_template(:index) + + expect(controller.instance_variable_get(:@notifications)).to be_empty + end + end + + context "user has notifications" do + let(:other_user) { FactoryBot.create(:user) } + let(:another_user) { FactoryBot.create(:user) } + let(:question) { FactoryBot.create(:question, user:) } + let!(:answer) { FactoryBot.create(:answer, question:, user: other_user) } + let!(:subscription) { Subscription.create(user:, answer:) } + let!(:comment) { FactoryBot.create(:comment, answer:, user: other_user) } + + it "should show a list of notifications" do + subject + expect(response).to render_template(:index) + expect(controller.instance_variable_get(:@notifications)).to have_attributes(size: 2) + end + + it "marks notifications as read" do + expect { subject }.to change { Notification.for(user).where(new: true).count }.from(2).to(0) + end + + it "updates the the timestamp used for caching" do + user.update(notifications_updated_at: original_notifications_updated_at) + travel 1.second do + expect { subject }.to change { user.reload.notifications_updated_at.floor }.from(original_notifications_updated_at.floor).to(Time.now.utc.floor) + end + end end end - context "user has notifications" do - let(:other_user) { FactoryBot.create(:user) } - let(:another_user) { FactoryBot.create(:user) } - let(:question) { FactoryBot.create(:question, user: user) } - let!(:answer) { FactoryBot.create(:answer, question: question, user: other_user) } - let!(:subscription) { Subscription.create(user: user, answer: answer) } - let!(:comment) { FactoryBot.create(:comment, answer: answer, user: other_user) } + describe "#read" do + subject { post :read, format: :turbo_stream } - it "should show a list of notifications" do - subject - expect(response).to render_template(:index) - expect(controller.instance_variable_get(:@notifications)).to have_attributes(size: 2) - end + let(:recipient) { FactoryBot.create(:user) } + let(:notification_count) { rand(8..20) } - it "marks notifications as read" do - expect { subject }.to change { Notification.for(user).where(new: true).count }.from(2).to(0) + context "user has some unread notifications" do + before do + notification_count.times do + FactoryBot.create(:user).follow(recipient) + end + + sign_in(recipient) + end + + it "marks all notifications as read" do + expect { subject }.to change { Notification.where(recipient:, new: true).count }.from(notification_count).to(0) + end end end end diff --git a/spec/controllers/question_controller_spec.rb b/spec/controllers/question_controller_spec.rb new file mode 100644 index 00000000..59baab9d --- /dev/null +++ b/spec/controllers/question_controller_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe QuestionController, type: :controller do + describe "#show" do + subject { get :show, params: { id: question.id, username: question.user.screen_name } } + + before do + stub_const("APP_CONFIG", { + "site_name" => "Specspring", + "hostname" => "test.host", + "https" => false, + "items_per_page" => 10, + }) + end + + context "question exists" do + let(:question) { FactoryBot.create(:question, user: FactoryBot.create(:user)) } + + context "no answers" do + it "renders an empty list" do + expect(subject).to have_http_status(:ok) + expect(assigns(:question)).to eq(question) + expect(assigns(:answers)).to be_empty + expect(assigns(:answers_last_id)).to be_nil + expect(assigns(:more_data_available)).to eq(false) + end + end + + context "some answers" do + before do + num_answers.times do + FactoryBot.create(:answer, question:, user: FactoryBot.create(:user)) + end + end + + let(:num_answers) { 10 } + + it "renders a list of questions" do + expect(subject).to have_http_status(:ok) + expect(assigns(:question)).to eq(question) + expect(assigns(:answers).length).to eq(10) + expect(assigns(:answers_last_id)).to_not be_nil + expect(assigns(:more_data_available)).to eq(false) + end + + context "enough answers to paginate" do + let(:num_answers) { 11 } + + it "renders a list of questions" do + expect(subject).to have_http_status(:ok) + expect(assigns(:question)).to eq(question) + expect(assigns(:answers).length).to eq(10) + expect(assigns(:answers_last_id)).to_not be_nil + expect(assigns(:more_data_available)).to eq(true) + end + end + + context "when signed in" do + before do + sign_in(FactoryBot.create(:user)) + end + + it "is fine" do + # basic test to make sure nothing breaks with an user + expect(subject).to have_http_status(:ok) + end + end + end + end + end +end diff --git a/spec/controllers/settings/profile_picture_controller_spec.rb b/spec/controllers/settings/profile_picture_controller_spec.rb index 6e1bc692..727e8f61 100644 --- a/spec/controllers/settings/profile_picture_controller_spec.rb +++ b/spec/controllers/settings/profile_picture_controller_spec.rb @@ -5,9 +5,18 @@ require "rails_helper" describe Settings::ProfilePictureController, type: :controller do describe "#update" do subject { patch :update, params: { user: avatar_params } } + + before do + if ENV["USE_FOG_IN_TESTS"].blank? + stub_const("APP_CONFIG", { + "fog" => {}, + }) + end + end + let(:avatar_params) do { - profile_picture: fixture_file_upload("banana_racc.jpg", "image/jpeg") + profile_picture: fixture_file_upload("banana_racc.jpg", "image/jpeg"), } end @@ -18,7 +27,7 @@ describe Settings::ProfilePictureController, type: :controller do it "enqueues a Sidekiq job to process the uploaded profile picture" do subject - expect(::CarrierWave::Workers::ProcessAsset).to have_enqueued_sidekiq_job("User", user.id.to_s, "profile_picture") + expect(CarrierWave::Workers::ProcessAsset).to have_enqueued_sidekiq_job("User", user.id.to_s, "profile_picture") end it "redirects to the edit_user_profile page" do diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index abec3188..15d7601b 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -3,50 +3,6 @@ require "rails_helper" describe ApplicationHelper, type: :helper do - describe "#inbox_count" do - context "no signed in user" do - it "should return 0 as inbox count" do - expect(helper.inbox_count).to eq(0) - end - end - - context "user is signed in" do - let(:user) { FactoryBot.create(:user) } - let(:question) { FactoryBot.create(:question) } - - before do - sign_in(user) - Inbox.create(user_id: user.id, question_id: question.id, new: true) - end - - it "should return the inbox count" do - expect(helper.inbox_count).to eq(1) - end - end - end - - describe "#notification_count" do - context "no signed in user" do - it "should return 0 as notification count" do - expect(helper.notification_count).to eq(0) - end - end - - context "user is signed in" do - let(:user) { FactoryBot.create(:user) } - let(:another_user) { FactoryBot.create(:user) } - - before do - sign_in(user) - another_user.follow(user) - end - - it "should return the notification count" do - expect(helper.notification_count).to eq(1) - end - end - end - describe "#privileged" do context "current user and checked user do not match" do let(:user) { FactoryBot.create(:user) } diff --git a/spec/helpers/social_helper/telegram_methods_spec.rb b/spec/helpers/social_helper/telegram_methods_spec.rb new file mode 100644 index 00000000..5e180373 --- /dev/null +++ b/spec/helpers/social_helper/telegram_methods_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe SocialHelper::TelegramMethods, type: :helper do + let(:user) { FactoryBot.create(:user) } + let(:answer) do + FactoryBot.create( + :answer, + user:, + content: "this is an answer\nwith multiple lines\nand **FORMATTING**", + question_content: "this is a question .... or is it?" + ) + end + + before do + stub_const("APP_CONFIG", { + "hostname" => "example.com", + "https" => true, + "items_per_page" => 5, + }) + end + + describe "#telegram_text" do + subject { telegram_text(answer) } + + it "returns a proper text for sharing" do + expect(subject).to eq(<<~TEXT.strip) + this is a question .... or is it? + ā€”ā€”ā€” + this is an answer + with multiple lines + and FORMATTING + TEXT + end + end + + describe "#telegram_share_url" do + subject { telegram_share_url(answer) } + + it "returns a proper share link" do + expect(subject).to eq(<<~URL.strip) + https://t.me/share/url?url=https%3A%2F%2Fexample.com%2F%40#{answer.user.screen_name}%2Fa%2F#{answer.id}&text=this+is+a+question+....+or+is+it%3F%0A%E2%80%94%E2%80%94%E2%80%94%0Athis+is+an+answer%0Awith+multiple+lines%0Aand+FORMATTING + URL + end + end +end diff --git a/spec/helpers/user_helper_spec.rb b/spec/helpers/user_helper_spec.rb index 1187070a..254c6b6e 100644 --- a/spec/helpers/user_helper_spec.rb +++ b/spec/helpers/user_helper_spec.rb @@ -123,7 +123,7 @@ describe UserHelper, type: :helper do context "user is not banned" do it "returns a link tag to the user's profile" do - expect(subject).to eq(link_to(user.profile.safe_name, user_path(user), class: "")) + expect(subject).to eq(link_to(user.profile.safe_name, user_path(user), class: "", target: "_top")) end end @@ -133,7 +133,7 @@ describe UserHelper, type: :helper do end it "returns a link tag to the user's profile" do - expect(subject).to eq(link_to(user.profile.safe_name, user_path(user), class: "user--banned")) + expect(subject).to eq(link_to(user.profile.safe_name, user_path(user), class: "user--banned", target: "_top")) end end end diff --git a/spec/lib/use_case/data_export/user_spec.rb b/spec/lib/use_case/data_export/user_spec.rb index 9f01e1ef..99d5d09e 100644 --- a/spec/lib/use_case/data_export/user_spec.rb +++ b/spec/lib/use_case/data_export/user_spec.rb @@ -3,6 +3,14 @@ require "rails_helper" describe UseCase::DataExport::User, :data_export do + before do + if ENV["USE_FOG_IN_TESTS"].blank? + stub_const("APP_CONFIG", { + "fog" => {}, + }) + end + end + let(:user_params) do { email: "fizzyraccoon@bsnss.biz", @@ -35,8 +43,8 @@ describe UseCase::DataExport::User, :data_export do location: "Binland", motivation_header: "", website: "https://retrospring.net", - allow_long_questions: true - } + allow_long_questions: true, + }, } end @@ -91,7 +99,7 @@ describe UseCase::DataExport::User, :data_export do privacy_noindex: false, sharing_enabled: false, sharing_autoclose: false, - sharing_custom_url: nil + sharing_custom_url: nil, }, profile: { display_name: "Fizzy Raccoon", @@ -102,12 +110,12 @@ describe UseCase::DataExport::User, :data_export do created_at: user.profile.created_at.as_json, updated_at: user.profile.updated_at.as_json, anon_display_name: nil, - allow_long_questions: true + allow_long_questions: true, }, roles: { administrator: false, - moderator: false - } + moderator: false, + }, } ) end diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb index 12fc9ed4..bca0c829 100644 --- a/spec/models/answer_spec.rb +++ b/spec/models/answer_spec.rb @@ -23,7 +23,7 @@ describe Answer, type: :model do context "user has the question in their inbox" do before do - Inbox.create(user: user, question: question, new: true) + Inbox.create(user:, question:, new: true) end it "should remove the question from the user's inbox" do @@ -91,4 +91,43 @@ describe Answer, type: :model do expect { subject.destroy }.to change { question.answer_count }.by(-1) end end + + describe ".public_timeline", timeline_test_data: true do + let(:user) { FactoryBot.create(:user) } + + subject { Answer.public_timeline } + + it "includes all answers to questions from all the users" do + expect(subject).to include(answer_to_anonymous) + expect(subject).to include(answer_to_normal_user) + expect(subject).to include(answer_to_normal_user_anonymous) + expect(subject).to include(answer_to_blocked_user_anonymous) + expect(subject).to include(answer_to_muted_user_anonymous) + expect(subject).to include(answer_to_blocked_user) + expect(subject).to include(answer_to_muted_user) + expect(subject).to include(answer_from_blocked_user) + expect(subject).to include(answer_from_muted_user) + end + + context "when given a current user who blocks and mutes some users" do + before do + user.block blocked_user + user.mute muted_user + end + + subject { Answer.public_timeline current_user: user } + + it "only includes answers to questions from users the user doesn't block or mute" do + expect(subject).to include(answer_to_anonymous) + expect(subject).to include(answer_to_normal_user) + expect(subject).to include(answer_to_normal_user_anonymous) + expect(subject).to include(answer_to_blocked_user_anonymous) + expect(subject).to include(answer_to_muted_user_anonymous) + expect(subject).not_to include(answer_to_blocked_user) + expect(subject).not_to include(answer_from_blocked_user) + expect(subject).not_to include(answer_to_muted_user) + expect(subject).not_to include(answer_from_muted_user) + end + end + end end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 4732d873..8cedd6d7 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -1,24 +1,24 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe(List, type: :model) do - let(:user) { FactoryBot.build(:user) } + let(:user) { FactoryBot.create(:user) } - describe 'name mangling' do + describe "name mangling" do subject do - List.new(user: user, display_name: display_name).tap(&:validate) + List.new(user:, display_name:).tap(&:validate) end { - 'great list' => 'great-list', - 'followers' => '-followers-', - ' followers ' => '-followers-', - " the game \t\nyes" => 'the-game-yes', + "great list" => "great-list", + "followers" => "-followers-", + " followers " => "-followers-", + " the game \t\nyes" => "the-game-yes", # not nice, but this is just the way it is: - "\u{1f98a} :3" => '3', - "\u{1f98a}" => '' + "\u{1f98a} :3" => "3", + "\u{1f98a}" => "", }.each do |display_name, expected_name| context "when display name is #{display_name.inspect}" do let(:display_name) { display_name } @@ -28,31 +28,31 @@ RSpec.describe(List, type: :model) do end end - describe 'validations' do + describe "validations" do subject do - List.new(user: user, display_name: display_name).validate + List.new(user:, display_name:).validate end context "when display name is 'great list' (valid)" do - let(:display_name) { 'great list' } + let(:display_name) { "great list" } it { is_expected.to be true } end context "when display name is '1' (valid)" do - let(:display_name) { '1' } + let(:display_name) { "1" } it { is_expected.to be true } end - context 'when display name is the letter E 621 times (invalid, too long)' do - let(:display_name) { 'E' * 621 } + context "when display name is the letter E 621 times (invalid, too long)" do + let(:display_name) { "E" * 621 } it { is_expected.to be false } end - context 'when display name is an empty string (invalid, as `name` would be empty)' do - let(:display_name) { '' } + context "when display name is an empty string (invalid, as `name` would be empty)" do + let(:display_name) { "" } it { is_expected.to be false } end @@ -63,4 +63,49 @@ RSpec.describe(List, type: :model) do it { is_expected.to be false } end end + + describe "#timeline", timeline_test_data: true do + let(:list) { List.create(user:, display_name: "test list") } + + before do + list.add_member user1 + list.add_member user2 + list.add_member blocked_user + list.add_member muted_user + + # block it here already, to test behaviour without a `current_user` passed in + user.block blocked_user + user.mute muted_user + end + + subject { list.timeline } + + it "includes all answers to questions from users in the list" do + expect(subject).to include(answer_to_anonymous) + expect(subject).to include(answer_to_normal_user) + expect(subject).to include(answer_to_normal_user_anonymous) + expect(subject).to include(answer_to_blocked_user_anonymous) + expect(subject).to include(answer_to_muted_user_anonymous) + expect(subject).to include(answer_to_blocked_user) + expect(subject).to include(answer_to_muted_user) + expect(subject).to include(answer_from_blocked_user) + expect(subject).to include(answer_from_muted_user) + end + + context "when given a current user who blocks and mutes some users" do + subject { list.timeline current_user: user } + + it "only includes answers to questions from users the user doesn't block or mute" do + expect(subject).to include(answer_to_anonymous) + expect(subject).to include(answer_to_normal_user) + expect(subject).to include(answer_to_normal_user_anonymous) + expect(subject).to include(answer_to_blocked_user_anonymous) + expect(subject).to include(answer_to_muted_user_anonymous) + expect(subject).not_to include(answer_to_blocked_user) + expect(subject).not_to include(answer_from_blocked_user) + expect(subject).not_to include(answer_to_muted_user) + expect(subject).not_to include(answer_from_muted_user) + end + end + end end diff --git a/spec/models/question_spec.rb b/spec/models/question_spec.rb index 7da9eab0..ab4e6cfc 100644 --- a/spec/models/question_spec.rb +++ b/spec/models/question_spec.rb @@ -1,30 +1,97 @@ -require 'rails_helper' +# frozen_string_literal: true -RSpec.describe Question, :type => :model do - before :each do - @question = Question.new( - content: 'Is this a question?', - user: FactoryBot.create(:user) - ) +require "rails_helper" + +RSpec.describe Question, type: :model do + let(:user) { FactoryBot.create :user } + + let(:question) { Question.new(content: "Is this a question?", user:) } + + subject { question } + + it { is_expected.to respond_to(:content) } + + describe "#content" do + it "returns a string" do + expect(question.content).to match "Is this a question?" + end end - subject { @question } + context "when it has many answers" do + before do + 5.times do |i| + Answer.create( + content: "This is an answer. #{i}", + user: FactoryBot.create(:user), + question: + ) + end + end - it { should respond_to(:content) } + its(:answer_count) { is_expected.to eq 5 } - it '#content returns a string' do - expect(@question.content).to match 'Is this a question?' + it "deletes the answers when deleted" do + first_answer_id = question.answers.first.id + question.destroy + expect { Answer.find(first_answer_id) }.to raise_error(ActiveRecord::RecordNotFound) + end end - it 'has many answers' do - 5.times { |i| Answer.create(content: "This is an answer. #{i}", user: FactoryBot.create(:user), question: @question) } - expect(@question.answer_count).to match 5 - end + describe "#ordered_answers" do + let(:normal_user) { FactoryBot.create(:user) } - it 'also deletes the answers when deleted' do - 5.times { |i| Answer.create(content: "This is an answer. #{i}", user: FactoryBot.create(:user), question: @question) } - first_answer_id = @question.answers.first.id - @question.destroy - expect{Answer.find(first_answer_id)}.to raise_error(ActiveRecord::RecordNotFound) + let(:blocked_user) { FactoryBot.create(:user) } + + let(:muted_user) { FactoryBot.create(:user) } + + let!(:answer_from_normal_user) do + FactoryBot.create( + :answer, + user: normal_user, + content: "answer from a normal user", + question: + ) + end + + let!(:answer_from_blocked_user) do + FactoryBot.create( + :answer, + user: blocked_user, + content: "answer from a blocked user", + question: + ) + end + + let!(:answer_from_muted_user) do + FactoryBot.create( + :answer, + user: muted_user, + content: "answer from a blocked user", + question: + ) + end + + subject { question.ordered_answers } + + it "includes all answers to questions" do + expect(subject).to include(answer_from_normal_user) + expect(subject).to include(answer_from_blocked_user) + expect(subject).to include(answer_from_muted_user) + end + + context "when given a current user who blocks and mutes some users" do + before do + user.block blocked_user + user.mute muted_user + end + + subject { question.ordered_answers current_user: user } + + it "only includes answers to questions from users the user doesn't block or mute" do + expect(subject).to include(answer_from_normal_user) + expect(subject).not_to include(answer_from_blocked_user) + expect(subject).not_to include(answer_from_muted_user) + end + end end end diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb new file mode 100644 index 00000000..eac76a29 --- /dev/null +++ b/spec/models/subscription_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Subscription do + describe "singleton object" do + describe "#notify" do + subject { Subscription.notify(source, target) } + + context "answer with one comment" do + let(:answer_author) { FactoryBot.create(:user) } + let(:answer) { FactoryBot.create(:answer, user: answer_author) } + let(:commenter) { FactoryBot.create(:user) } + let!(:comment) { FactoryBot.create(:comment, answer:, user: commenter) } + let(:source) { comment } + let(:target) { answer } + + it "notifies the target about source" do + # The method we're testing here is already called the +after_create+ of +Comment+ so there already is a notification + expect { subject }.to change { Notification.count }.from(1).to(2) + created = Notification.order(:created_at).first! + expect(created.target).to eq(comment) + expect(created.recipient).to eq(answer_author) + end + end + end + end +end diff --git a/spec/models/user/inbox_methods_spec.rb b/spec/models/user/inbox_methods_spec.rb new file mode 100644 index 00000000..efa7d16e --- /dev/null +++ b/spec/models/user/inbox_methods_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe User::InboxMethods do + context "given a user" do + let(:user) { FactoryBot.create(:user) } + + describe "#unread_inbox_count" do + subject { user.unread_inbox_count } + + context "user has no questions in their inbox" do + it "should return nil" do + expect(subject).to eq(nil) + end + end + + context "user has 1 question in their inbox" do + # FactoryBot seems to have issues with setting the +new+ field on inbox entries + # so we can create it manually instead + let!(:inbox) { Inbox.create(question: FactoryBot.create(:question), user:, new: true) } + + it "should return 1" do + expect(subject).to eq(1) + end + end + end + end +end diff --git a/spec/models/user/notification_methods_spec.rb b/spec/models/user/notification_methods_spec.rb new file mode 100644 index 00000000..550d3116 --- /dev/null +++ b/spec/models/user/notification_methods_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe User::NotificationMethods do + context "given a user" do + let(:user) { FactoryBot.create(:user) } + + describe "#unread_notification_count" do + subject { user.unread_notification_count } + + context "user has no notifications" do + it "should return nil" do + expect(subject).to eq(nil) + end + end + + context "user has a notification" do + let(:other_user) { FactoryBot.create(:user) } + + before do + other_user.follow(user) + end + + it "should return 1" do + expect(subject).to eq(1) + end + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 01d3fc47..03ec76f2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -10,7 +10,7 @@ RSpec.describe User, type: :model do @user = User.new( screen_name: "FunnyMeme2004", password: "y_u_no_secure_password?", - email: "nice.meme@nsa.gov" + email: "nice.meme@nsa.gov", ) Profile.new(user: @user) end @@ -33,6 +33,45 @@ RSpec.describe User, type: :model do end end + describe "callbacks" do + describe "before_destroy" do + it "marks reports about this user as deleted" do + other_user = FactoryBot.create(:user) + other_user.report me, "va tutto benissimo" + + expect { me.destroy } + .to change { Reports::User.find_by(target_id: me.id).deleted? } + .from(false) + .to(true) + end + end + + describe "after_destroy" do + it "increments the users_destroyed metric" do + expect { me.destroy }.to change { Retrospring::Metrics::USERS_DESTROYED.values.values.sum }.by(1) + end + end + + describe "after_create" do + subject :user do + User.create!( + screen_name: "konqi", + email: "konqi@example.rrerr.net", + password: "dragonsRQt5", + ) + end + + it "creates a profile for the user" do + expect { user }.to change { Profile.count }.by(1) + expect(Profile.find_by(user:).user).to eq(user) + end + + it "increments the users_created metric" do + expect { user }.to change { Retrospring::Metrics::USERS_CREATED.values.values.sum }.by(1) + end + end + end + describe "custom sharing url validation" do subject do FactoryBot.build(:user, sharing_custom_url: url).tap(&:validate).errors[:sharing_custom_url] @@ -69,7 +108,7 @@ RSpec.describe User, type: :model do describe "email validation" do subject do - FactoryBot.build(:user, email: email).tap(&:validate).errors[:email] + FactoryBot.build(:user, email:).tap(&:validate).errors[:email] end shared_examples_for "valid email" do |example_email| @@ -130,6 +169,7 @@ RSpec.describe User, type: :model do include_examples "invalid email", "fritz.fantom@gmali.com" include_examples "invalid email", "fritz.fantom@gmaul.com" include_examples "invalid email", "fritz.fantom@gnail.com" + include_examples "invalid email", "fritz.fantom@hornail.com" include_examples "invalid email", "fritz.fantom@hotamil.com" include_examples "invalid email", "fritz.fantom@hotmai.com" include_examples "invalid email", "fritz.fantom@hotmailcom" @@ -137,6 +177,7 @@ RSpec.describe User, type: :model do include_examples "invalid email", "fritz.fantom@iclooud.com" include_examples "invalid email", "fritz.fantom@iclould.com" include_examples "invalid email", "fritz.fantom@icluod.com" + include_examples "invalid email", "fritz.fantom@maibox.org" include_examples "invalid email", "fritz.fantom@protonail.com" include_examples "invalid email", "fritz.fantom@xn--gmail-xk1c.com" include_examples "invalid email", "fritz.fantom@yahooo.com" @@ -210,12 +251,50 @@ RSpec.describe User, type: :model do expect(subject).to eq(expected) end end + + context "user follows users with answers to questions from blocked or muted users", timeline_test_data: true do + before do + me.follow user1 + me.follow user2 + end + + it "includes all answers to questions the user follows" do + expect(subject).to include(answer_to_anonymous) + expect(subject).to include(answer_to_normal_user) + expect(subject).to include(answer_to_normal_user_anonymous) + expect(subject).to include(answer_to_blocked_user_anonymous) + expect(subject).to include(answer_to_muted_user_anonymous) + expect(subject).to include(answer_to_blocked_user) + expect(subject).to include(answer_to_muted_user) + expect(subject).not_to include(answer_from_blocked_user) + expect(subject).not_to include(answer_from_muted_user) + end + + context "when blocking and muting some users" do + before do + me.block blocked_user + me.mute muted_user + end + + it "only includes answers to questions from users the user doesn't block or mute" do + expect(subject).to include(answer_to_anonymous) + expect(subject).to include(answer_to_normal_user) + expect(subject).to include(answer_to_normal_user_anonymous) + expect(subject).to include(answer_to_blocked_user_anonymous) + expect(subject).to include(answer_to_muted_user_anonymous) + expect(subject).not_to include(answer_to_blocked_user) + expect(subject).not_to include(answer_to_muted_user) + expect(subject).not_to include(answer_from_blocked_user) + expect(subject).not_to include(answer_from_muted_user) + end + end + end end describe "#cursored_timeline" do let(:last_id) { nil } - subject { me.cursored_timeline(last_id: last_id, size: 3) } + subject { me.cursored_timeline(last_id:, size: 3) } context "user answered nothing and is not following anyone" do include_examples "result is blank" diff --git a/spec/shared_examples/timeline_test_data.rb b/spec/shared_examples/timeline_test_data.rb new file mode 100644 index 00000000..06ae644b --- /dev/null +++ b/spec/shared_examples/timeline_test_data.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +RSpec.shared_context "Timeline test data" do + let(:user1) { FactoryBot.create(:user) } + + let(:user2) { FactoryBot.create(:user) } + + let(:blocked_user) { FactoryBot.create(:user) } + + let(:muted_user) { FactoryBot.create(:user) } + + let!(:answer_to_anonymous) do + FactoryBot.create( + :answer, + user: user1, + content: "answer to a true anonymous coward", + question: FactoryBot.create( + :question, + author_is_anonymous: true + ) + ) + end + + let!(:answer_to_normal_user) do + FactoryBot.create( + :answer, + user: user2, + content: "answer to a normal user", + question: FactoryBot.create( + :question, + user: user1, + author_is_anonymous: false + ) + ) + end + + let!(:answer_to_normal_user_anonymous) do + FactoryBot.create( + :answer, + user: user2, + content: "answer to a cowardly user", + question: FactoryBot.create( + :question, + user: user1, + author_is_anonymous: true + ) + ) + end + + let!(:answer_from_blocked_user) do + FactoryBot.create( + :answer, + user: blocked_user, + content: "answer from a blocked user", + question: FactoryBot.create(:question) + ) + end + + let!(:answer_to_blocked_user) do + FactoryBot.create( + :answer, + user: user1, + content: "answer to a blocked user", + question: FactoryBot.create( + :question, + user: blocked_user, + author_is_anonymous: false + ) + ) + end + + let!(:answer_to_blocked_user_anonymous) do + FactoryBot.create( + :answer, + user: user1, + content: "answer to a blocked user who's a coward", + question: FactoryBot.create( + :question, + user: blocked_user, + author_is_anonymous: true + ) + ) + end + + let!(:answer_from_muted_user) do + FactoryBot.create( + :answer, + user: muted_user, + content: "answer from a muted user", + question: FactoryBot.create(:question) + ) + end + + let!(:answer_to_muted_user) do + FactoryBot.create( + :answer, + user: user2, + content: "answer to a muted user", + question: FactoryBot.create( + :question, + user: muted_user, + author_is_anonymous: false + ) + ) + end + + let!(:answer_to_muted_user_anonymous) do + FactoryBot.create( + :answer, + user: user2, + content: "answer to a muted user who's a coward", + question: FactoryBot.create( + :question, + user: muted_user, + author_is_anonymous: true + ) + ) + end +end + +RSpec.configure do |config| + config.include_context "Timeline test data", timeline_test_data: true +end diff --git a/spec/views/actions/_share.html.haml_spec.rb b/spec/views/actions/_share.html.haml_spec.rb new file mode 100644 index 00000000..16153d1c --- /dev/null +++ b/spec/views/actions/_share.html.haml_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "actions/_share.html.haml", type: :view do + let(:answer) { FactoryBot.create(:answer, user: FactoryBot.create(:user)) } + + subject(:rendered) do + render partial: "actions/share", locals: { + answer:, + } + end + + it "has a dropdown item to share to twitter" do + expect(rendered).to have_css(%(a.dropdown-item[href^="https://twitter.com/"][target="_blank"])) + end + + it "has a dropdown item to share to tumblr" do + expect(rendered).to have_css(%(a.dropdown-item[href^="https://www.tumblr.com/"][target="_blank"])) + end + + it "has a dropdown item to share to telegram" do + expect(rendered).to have_css(%(a.dropdown-item[href^="https://t.me/"][target="_blank"])) + end + + it "has a dropdown item to share to anywhere else" do + expect(rendered).to have_css(%(a.dropdown-item[name="ab-share"])) + end +end diff --git a/spec/views/inbox/_entry.html.haml_spec.rb b/spec/views/inbox/_entry.html.haml_spec.rb index 72243982..a94c2c1c 100644 --- a/spec/views/inbox/_entry.html.haml_spec.rb +++ b/spec/views/inbox/_entry.html.haml_spec.rb @@ -82,6 +82,10 @@ describe "inbox/_entry.html.haml", type: :view do expect(rendered).to have_css(%(.inbox-entry__sharing.d-none)) end + it "has a link-button to share to telegram" do + expect(rendered).to have_css(%(.inbox-entry__sharing a.btn[data-inbox-sharing-target="telegram"])) + end + it "has a link-button to share to tumblr" do expect(rendered).to have_css(%(.inbox-entry__sharing a.btn[data-inbox-sharing-target="tumblr"])) end diff --git a/spec/workers/question_worker_spec.rb b/spec/workers/question_worker_spec.rb index 8070392b..0795e55b 100644 --- a/spec/workers/question_worker_spec.rb +++ b/spec/workers/question_worker_spec.rb @@ -78,7 +78,7 @@ describe QuestionWorker do user: receiver, subscription: { endpoint: "This will not be used", - keys: {} + keys: {}, } ) receiver.follow(user) diff --git a/yarn.lock b/yarn.lock index 6324ee68..2bb71921 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,120 +44,135 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@csstools/selector-specificity@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" - integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== +"@csstools/css-parser-algorithms@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz#0cc3a656dc2d638370ecf6f98358973bfbd00141" + integrity sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA== -"@esbuild/android-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz#b3d5b65a3b2e073a6c7ee36b1f3c30c8f000315b" - integrity sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ== +"@csstools/css-tokenizer@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e" + integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA== -"@esbuild/android-arm@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.8.tgz#c41e496af541e175369d48164d0cf01a5f656cf6" - integrity sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w== +"@csstools/media-query-list-parser@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz#6ef642b728d30c1009bfbba3211c7e4c11302728" + integrity sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ== -"@esbuild/android-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.8.tgz#080fa67c29be77f5a3ca5ee4cc78d5bf927e3a3b" - integrity sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg== +"@csstools/selector-specificity@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz#798622546b63847e82389e473fd67f2707d82247" + integrity sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g== -"@esbuild/darwin-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz#053622bf9a82f43d5c075b7818e02618f7b4a397" - integrity sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg== +"@esbuild/android-arm64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.11.tgz#fa6f0cc7105367cb79cc0a8bf32bf50cb1673e45" + integrity sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw== -"@esbuild/darwin-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz#8a1aadb358d537d8efad817bb1a5bff91b84734b" - integrity sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw== +"@esbuild/android-arm@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.11.tgz#ae84a410696c9f549a15be94eaececb860bacacb" + integrity sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q== -"@esbuild/freebsd-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz#e6738d0081ba0721a5c6c674e84c6e7fcea61989" - integrity sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A== +"@esbuild/android-x64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.11.tgz#0e58360bbc789ad0d68174d32ba20e678c2a16b6" + integrity sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw== -"@esbuild/freebsd-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz#1855e562f2b730f4483f6e94086e9e2597feb4c3" - integrity sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q== +"@esbuild/darwin-arm64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz#fcdcd2ef76ca656540208afdd84f284072f0d1f9" + integrity sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w== -"@esbuild/linux-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz#481da38952721a3fdb77c17a36ceaacc4270b5c5" - integrity sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww== +"@esbuild/darwin-x64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.11.tgz#c5ac602ec0504a8ff81e876bc8a9811e94d69d37" + integrity sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw== -"@esbuild/linux-arm@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz#18127072b270bb6321c6d11be20bfd30e0d6ad17" - integrity sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA== +"@esbuild/freebsd-arm64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.11.tgz#7012fb06ee3e6e0d5560664a65f3fefbcc46db2e" + integrity sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A== -"@esbuild/linux-ia32@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz#ee400af7b3bc69e8ca2e593ca35156ffb9abd54f" - integrity sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA== +"@esbuild/freebsd-x64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.11.tgz#c5de1199f70e1f97d5c8fca51afa9bf9a2af5969" + integrity sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q== -"@esbuild/linux-loong64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz#8c509d8a454693d39824b83b3f66c400872fce82" - integrity sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA== +"@esbuild/linux-arm64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.11.tgz#2a6d3a74e0b8b5f294e22b4515b29f76ebd42660" + integrity sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog== -"@esbuild/linux-mips64el@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz#f2b0d36e63fb26bc3f95b203b6a80638292101ca" - integrity sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA== +"@esbuild/linux-arm@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.11.tgz#5175bd61b793b436e4aece6328aa0d9be07751e1" + integrity sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg== -"@esbuild/linux-ppc64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz#1e628be003e036e90423716028cc884fe5ba25bd" - integrity sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw== +"@esbuild/linux-ia32@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.11.tgz#20ee6cfd65a398875f321a485e7b2278e5f6f67b" + integrity sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw== -"@esbuild/linux-riscv64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz#419a815cb4c3fb9f1b78ef5295f5b48b8bf6427a" - integrity sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw== +"@esbuild/linux-loong64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.11.tgz#8e7b251dede75083bf44508dab5edce3f49d052b" + integrity sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw== -"@esbuild/linux-s390x@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz#291c49ae5c3d11d226352755c0835911fe1a9e5c" - integrity sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw== +"@esbuild/linux-mips64el@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.11.tgz#a3125eb48538ac4932a9d05089b157f94e443165" + integrity sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg== -"@esbuild/linux-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz#03199d91c76faf80bd54104f5cbf0a489bc39f6a" - integrity sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA== +"@esbuild/linux-ppc64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.11.tgz#842abadb7a0995bd539adee2be4d681b68279499" + integrity sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ== -"@esbuild/netbsd-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz#b436d767e1b21852f9ed212e2bb57f77203b0ae2" - integrity sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q== +"@esbuild/linux-riscv64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.11.tgz#7ce6e6cee1c72d5b4d2f4f8b6fcccf4a9bea0e28" + integrity sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w== -"@esbuild/openbsd-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz#d1481d8539e21d4729cd04a0450a26c2c8789e89" - integrity sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg== +"@esbuild/linux-s390x@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.11.tgz#98fbc794363d02ded07d300df2e535650b297b96" + integrity sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg== -"@esbuild/sunos-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz#2cfb8126e079b2c00fd1bf095541e9f5c47877e4" - integrity sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw== +"@esbuild/linux-x64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.11.tgz#f8458ec8cf74c8274e4cacd00744d8446cac52eb" + integrity sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA== -"@esbuild/win32-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz#7c6ecfd097ca23b82119753bf7072bbaefe51e3a" - integrity sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg== +"@esbuild/netbsd-x64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.11.tgz#a7b2f991b8293748a7be42eac1c4325faf0c7cca" + integrity sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q== -"@esbuild/win32-ia32@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz#cffec63c3cb0ef8563a04df4e09fa71056171d00" - integrity sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w== +"@esbuild/openbsd-x64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.11.tgz#3e50923de84c54008f834221130fd23646072b2f" + integrity sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ== -"@esbuild/win32-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz#200a0965cf654ac28b971358ecdca9cc5b44c335" - integrity sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg== +"@esbuild/sunos-x64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.11.tgz#ae47a550b0cd395de03606ecfba03cc96c7c19e2" + integrity sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng== + +"@esbuild/win32-arm64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.11.tgz#05d364582b7862d7fbf4698ef43644f7346dcfcc" + integrity sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg== + +"@esbuild/win32-ia32@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.11.tgz#a3372095a4a1939da672156a3c104f8ce85ee616" + integrity sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg== + +"@esbuild/win32-x64@0.18.11": + version "0.18.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz#6526c7e1b40d5b9f0a222c6b767c22f6fb97aa57" + integrity sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA== "@eslint/eslintrc@^0.4.3": version "0.4.3" @@ -179,28 +194,33 @@ resolved "https://registry.yarnpkg.com/@fontsource/lexend/-/lexend-4.5.15.tgz#ee033b850224d05b665d6661bf8d32c950f166bb" integrity sha512-6edLmDmte8pJWtQ1NIahcJq0iWlf6b0/JLwkd7WbDQ3C5tZWnxjvbP4RSklMv4MPzGjpuYuYnc+QANbjUws1oA== -"@fortawesome/fontawesome-free@^6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz#b5877182692a6f7a39d1108837bec24247ba4bd7" - integrity sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA== +"@fortawesome/fontawesome-free@^6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz#1ee0c174e472c84b23cb46c995154dc383e3b4fe" + integrity sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ== + +"@github/hotkey@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@github/hotkey/-/hotkey-2.0.1.tgz#24ad6b49313cee5b98368174eab16a4b53a08ec7" + integrity sha512-qKXjAJjtheJbf4ie3hi8IwrHWJZHB5qdojR6JGo6jvQNPpsdUbk/NIdU8sxu4PW41CjW80vfciDMu3MAP3j2Fg== "@hotwired/stimulus@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.1.tgz#e3de23623b0c52c247aba4cd5d530d257008676b" integrity sha512-HGlzDcf9vv/EQrMJ5ZG6VWNs8Z/xMN+1o2OhV1gKiSG6CqZt5MCBB1gRg5ILiN3U0jEAxuDTNPRfBcnZBDmupQ== -"@hotwired/turbo-rails@^7.2.5": - version "7.2.5" - resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-7.2.5.tgz#74fc3395a29a76df2bb8835aa88c86885cffde4c" - integrity sha512-F8ztmARxd/XBdevRa//HoJGZ7u+Unb0J7cQUeUP+pBvt9Ta2TJJ7a2TORAOhjC8Zgxx+LKwm/1UUHqN3ojjiGw== +"@hotwired/turbo-rails@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-7.3.0.tgz#422c21752509f3edcd6c7b2725bbe9e157815f51" + integrity sha512-fvhO64vp/a2UVQ3jue9WTc2JisMv9XilIC7ViZmXAREVwiQ2S4UC7Go8f9A1j4Xu7DBI6SbFdqILk5ImqVoqyA== dependencies: - "@hotwired/turbo" "^7.2.5" + "@hotwired/turbo" "^7.3.0" "@rails/actioncable" "^7.0" -"@hotwired/turbo@^7.2.5": - version "7.2.5" - resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.2.5.tgz#2d9d6bde8a9549c3aea8970445ade16ffd56719a" - integrity sha512-o5PByC/mWkmTe4pWnKrixhPECJUxIT/NHtxKqjq7n9Fj6JlNza1pgxdTCJVIq+PI0j95U+7mA3N4n4A/QYZtZQ== +"@hotwired/turbo@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.3.0.tgz#2226000fff1aabda9fd9587474565c9929dbf15d" + integrity sha512-Dcu+NaSvHLT7EjrDrkEmH4qET2ZJZ5IcCWmNXxNQTBwlnE5tBZfN6WxZ842n5cHV52DH/AKNirbPBtcEXDLW4g== "@humanwhocodes/config-array@^0.5.0": version "0.5.0" @@ -216,10 +236,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -"@melloware/coloris@^0.17.1": - version "0.17.1" - resolved "https://registry.yarnpkg.com/@melloware/coloris/-/coloris-0.17.1.tgz#06b16fa8a9bbdbc6f5005953e5f1946949c10f52" - integrity sha512-EF+XlJEPcn4amdiOnXqhdZLSHfgVugVv3I538lypFAcyskc8DSx/GY5/oTLLSf83t1BeMcoee9JxFC9r5Xnr5w== +"@melloware/coloris@^0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@melloware/coloris/-/coloris-0.21.0.tgz#690fba88db0eb075cbaa762de0540f6f01f8fbf4" + integrity sha512-jy4+XS9yv66dDwp14HK2eRTrmHP57nha1ZY2RP7TWsYJ2L+pZynMIiru8zL40BjAnlRFbWmTat1TS229mScr7g== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -243,9 +263,9 @@ fastq "^1.6.0" "@popperjs/core@^2.11": - version "2.11.6" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" - integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== "@rails/actioncable@^7.0": version "7.0.3" @@ -267,7 +287,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/minimist@^1.2.0": +"@types/minimist@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== @@ -277,11 +297,6 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - "@typescript-eslint/eslint-plugin@^4.11.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" @@ -421,6 +436,19 @@ argparse@^1.0.7: 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== + +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + array-includes@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" @@ -437,6 +465,17 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array.prototype.findlastindex@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz#bc229aef98f6bd0533a2bc61ff95209875526c9b" + integrity sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw== + 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" + 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" @@ -457,6 +496,18 @@ array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +arraybuffer.prototype.slice@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb" + integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -510,7 +561,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -538,19 +589,20 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== +camelcase-keys@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-7.0.2.tgz#d048d8c69448745bb0de6fc4c1c52a30dfbe7252" + integrity sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg== dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" + camelcase "^6.3.0" + map-obj "^4.1.0" + quick-lru "^5.1.1" + type-fest "^1.2.1" -camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== chalk@^2.0.0: version "2.4.2" @@ -623,16 +675,15 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -cosmiconfig@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" - integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== +cosmiconfig@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" + integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== dependencies: - "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" + js-yaml "^4.1.0" parse-json "^5.0.0" path-type "^4.0.0" - yaml "^1.10.0" croppr@^2.3.1: version "2.3.1" @@ -648,10 +699,18 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -css-functions-list@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b" - integrity sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w== +css-functions-list@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.0.tgz#8290b7d064bf483f48d6559c10e98dc4d1ad19ee" + integrity sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg== + +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" cssesc@^3.0.0: version "3.0.0" @@ -680,11 +739,16 @@ decamelize-keys@^1.1.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.2.0: +decamelize@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decamelize@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" + integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -705,6 +769,14 @@ define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-properties@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" + integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -810,6 +882,51 @@ es-abstract@^1.20.4: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-abstract@^1.21.2: + version "1.22.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" + integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.1" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.2.1" + 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.5" + is-array-buffer "^3.0.2" + 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.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.0" + safe-array-concat "^1.0.0" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.7" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.10" + 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" @@ -835,33 +952,33 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -esbuild@^0.17.8: - version "0.17.8" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.8.tgz#f7f799abc7cdce3f0f2e3e0c01f120d4d55193b4" - integrity sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g== +esbuild@^0.18.11: + version "0.18.11" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.11.tgz#cbf94dc3359d57f600a0dbf281df9b1d1b4a156e" + integrity sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA== optionalDependencies: - "@esbuild/android-arm" "0.17.8" - "@esbuild/android-arm64" "0.17.8" - "@esbuild/android-x64" "0.17.8" - "@esbuild/darwin-arm64" "0.17.8" - "@esbuild/darwin-x64" "0.17.8" - "@esbuild/freebsd-arm64" "0.17.8" - "@esbuild/freebsd-x64" "0.17.8" - "@esbuild/linux-arm" "0.17.8" - "@esbuild/linux-arm64" "0.17.8" - "@esbuild/linux-ia32" "0.17.8" - "@esbuild/linux-loong64" "0.17.8" - "@esbuild/linux-mips64el" "0.17.8" - "@esbuild/linux-ppc64" "0.17.8" - "@esbuild/linux-riscv64" "0.17.8" - "@esbuild/linux-s390x" "0.17.8" - "@esbuild/linux-x64" "0.17.8" - "@esbuild/netbsd-x64" "0.17.8" - "@esbuild/openbsd-x64" "0.17.8" - "@esbuild/sunos-x64" "0.17.8" - "@esbuild/win32-arm64" "0.17.8" - "@esbuild/win32-ia32" "0.17.8" - "@esbuild/win32-x64" "0.17.8" + "@esbuild/android-arm" "0.18.11" + "@esbuild/android-arm64" "0.18.11" + "@esbuild/android-x64" "0.18.11" + "@esbuild/darwin-arm64" "0.18.11" + "@esbuild/darwin-x64" "0.18.11" + "@esbuild/freebsd-arm64" "0.18.11" + "@esbuild/freebsd-x64" "0.18.11" + "@esbuild/linux-arm" "0.18.11" + "@esbuild/linux-arm64" "0.18.11" + "@esbuild/linux-ia32" "0.18.11" + "@esbuild/linux-loong64" "0.18.11" + "@esbuild/linux-mips64el" "0.18.11" + "@esbuild/linux-ppc64" "0.18.11" + "@esbuild/linux-riscv64" "0.18.11" + "@esbuild/linux-s390x" "0.18.11" + "@esbuild/linux-x64" "0.18.11" + "@esbuild/netbsd-x64" "0.18.11" + "@esbuild/openbsd-x64" "0.18.11" + "@esbuild/sunos-x64" "0.18.11" + "@esbuild/win32-arm64" "0.18.11" + "@esbuild/win32-ia32" "0.18.11" + "@esbuild/win32-x64" "0.18.11" escape-string-regexp@^1.0.5: version "1.0.5" @@ -882,33 +999,36 @@ eslint-import-resolver-node@^0.3.7: is-core-module "^2.11.0" resolve "^1.22.1" -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== +eslint-module-utils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== dependencies: debug "^3.2.7" -eslint-plugin-import@^2.27.5: - version "2.27.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" - integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== +eslint-plugin-import@^2.28.0: + version "2.28.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz#8d66d6925117b06c4018d491ae84469eb3cb1005" + integrity sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q== dependencies: array-includes "^3.1.6" + array.prototype.findlastindex "^1.2.2" array.prototype.flat "^1.3.1" array.prototype.flatmap "^1.3.1" debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.7" - eslint-module-utils "^2.7.4" + eslint-module-utils "^2.8.0" has "^1.0.3" - is-core-module "^2.11.0" + is-core-module "^2.12.1" is-glob "^4.0.3" minimatch "^3.1.2" + object.fromentries "^2.0.6" + object.groupby "^1.0.0" object.values "^1.1.6" - resolve "^1.22.1" - semver "^6.3.0" - tsconfig-paths "^3.14.1" + resolve "^1.22.3" + semver "^6.3.1" + tsconfig-paths "^3.14.2" eslint-scope@^5.1.1: version "5.1.1" @@ -1036,21 +1156,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.12: - 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" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@^3.2.9: - version "3.2.10" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.10.tgz#2734f83baa7f43b7fd41e13bc34438f4ffe284ee" - integrity sha512-s9nFhFnvR63wls6/kM88kQqDhMu0AfdjqouE2l5GVQPbqLgyFjjU5ry/r2yKsJxpb9Py1EYNqieFrmMaX4v++A== +fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0" + integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1094,12 +1203,12 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== +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 "^5.0.0" + locate-path "^6.0.0" path-exists "^4.0.0" flat-cache@^3.0.4: @@ -1152,7 +1261,7 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -functions-have-names@^1.2.2: +functions-have-names@^1.2.2, functions-have-names@^1.2.3: 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== @@ -1175,6 +1284,16 @@ get-intrinsic@^1.1.3: has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -1317,11 +1436,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - hosted-git-info@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" @@ -1329,10 +1443,10 @@ hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" -html-tags@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" - integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== +html-tags@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== i18n-js@^4.0: version "4.1.1" @@ -1352,12 +1466,7 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.8, ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -ignore@^5.2.1: +ignore@^5.1.8, ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -1385,10 +1494,10 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== inflight@^1.0.4: version "1.0.6" @@ -1426,6 +1535,15 @@ internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + 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" @@ -1435,6 +1553,15 @@ is-array-buffer@^3.0.1: get-intrinsic "^1.1.3" is-typed-array "^1.1.10" +is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -1472,10 +1599,10 @@ is-callable@^1.1.4, is-callable@^1.2.4: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== -is-core-module@^2.11.0, is-core-module@^2.5.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== +is-core-module@^2.11.0, is-core-module@^2.12.0, is-core-module@^2.12.1, is-core-module@^2.5.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== dependencies: has "^1.0.3" @@ -1582,6 +1709,11 @@ is-weakref@^1.0.1, is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +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== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1605,6 +1737,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +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 "^2.0.1" + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -1625,7 +1764,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json5@^1.0.1: +json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== @@ -1637,10 +1776,10 @@ kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -known-css-properties@^0.26.0: - version "0.26.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.26.0.tgz#008295115abddc045a9f4ed7e2a84dc8b3a77649" - integrity sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg== +known-css-properties@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" + integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg== levn@^0.4.1: version "0.4.1" @@ -1655,12 +1794,12 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== +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 "^4.1.0" + p-locate "^5.0.0" lodash.merge@^4.6.2: version "4.6.2" @@ -1672,7 +1811,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@*, lodash@^4.17.21: +lodash@*: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1689,7 +1828,7 @@ map-obj@^1.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== -map-obj@^4.0.0: +map-obj@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== @@ -1699,38 +1838,35 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -meow@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" - integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + +meow@^10.1.5: + version "10.1.5" + resolved "https://registry.yarnpkg.com/meow/-/meow-10.1.5.tgz#be52a1d87b5f5698602b0f32875ee5940904aa7f" + integrity sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw== dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize "^1.2.0" + "@types/minimist" "^1.2.2" + camelcase-keys "^7.0.0" + decamelize "^5.0.0" decamelize-keys "^1.1.0" hard-rejection "^2.1.0" minimist-options "4.1.0" - normalize-package-data "^3.0.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.18.0" - yargs-parser "^20.2.3" + normalize-package-data "^3.0.2" + read-pkg-up "^8.0.0" + redent "^4.0.0" + trim-newlines "^4.0.2" + type-fest "^1.2.2" + yargs-parser "^20.2.9" merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== - dependencies: - braces "^3.0.1" - picomatch "^2.2.3" - -micromatch@^4.0.5: +micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -1738,7 +1874,7 @@ micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -min-indent@^1.0.0: +min-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== @@ -1774,27 +1910,17 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -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== +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - 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-package-data@^3.0.0: +normalize-package-data@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== @@ -1814,7 +1940,7 @@ object-inspect@^1.11.0, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== -object-inspect@^1.12.2: +object-inspect@^1.12.2, object-inspect@^1.12.3: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== @@ -1844,6 +1970,25 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" +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.4" + es-abstract "^1.20.4" + +object.groupby@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.0.tgz#cb29259cf90f37e7bac6437686c1ea8c916d12a9" + integrity sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.21.2" + get-intrinsic "^1.2.1" + object.values@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" @@ -1872,24 +2017,19 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== +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 "^2.0.0" + yocto-queue "^0.1.0" -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== +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 "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + p-limit "^3.0.2" parent-module@^1.0.0: version "1.0.1" @@ -1898,7 +2038,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -1938,7 +2078,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -1958,30 +2098,30 @@ postcss-safe-parser@^6.0.0: resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== -postcss-scss@^4.0.2: +postcss-scss@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.6.tgz#5d62a574b950a6ae12f2aa89b60d63d9e4432bfd" integrity sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ== -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.6: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== +postcss-selector-parser@^6.0.13: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" + integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.19: - version "8.4.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== +postcss@^8.4.25: + version "8.4.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" + integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== dependencies: - nanoid "^3.3.4" + nanoid "^3.3.6" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -2005,29 +2145,29 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== +read-pkg-up@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-8.0.0.tgz#72f595b65e66110f43b052dd9af4de6b10534670" + integrity sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ== dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" + find-up "^5.0.0" + read-pkg "^6.0.0" + type-fest "^1.0.1" -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== +read-pkg@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-6.0.0.tgz#a67a7d6a1c2b0c3cd6aa2ea521f40c458a4a504c" + integrity sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q== dependencies: "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" + normalize-package-data "^3.0.2" + parse-json "^5.2.0" + type-fest "^1.0.1" readdirp@~3.6.0: version "3.6.0" @@ -2036,13 +2176,13 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== +redent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-4.0.0.tgz#0c0ba7caabb24257ab3bb7a4fd95dd1d5c5681f9" + integrity sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag== dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" + indent-string "^5.0.0" + strip-indent "^4.0.0" regexp.prototype.flags@^1.4.3: version "1.4.3" @@ -2053,6 +2193,15 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" +regexp.prototype.flags@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" + integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + functions-have-names "^1.2.3" + regexpp@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -2073,12 +2222,12 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.10.0, 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== +resolve@^1.22.1, resolve@^1.22.3: + version "1.22.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.3.tgz#4b4055349ffb962600972da1fdc33c46a4eb3283" + integrity sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.12.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -2101,6 +2250,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +safe-array-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" + integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + has-symbols "^1.0.3" + isarray "^2.0.5" + 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" @@ -2110,29 +2269,24 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -sass@^1.58.0: - version "1.58.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.58.0.tgz#ee8aea3ad5ea5c485c26b3096e2df6087d0bb1cc" - integrity sha512-PiMJcP33DdKtZ/1jSjjqVIKihoDc6yWmYr9K/4r3fVVIEDAluD0q7XZiRKrNJcPK3qkLRF/79DND1H5q1LBjgg== +sass@^1.64.1: + version "1.64.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.64.1.tgz#6a46f6d68e0fa5ad90aa59ce025673ddaa8441cf" + integrity sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -"semver@2 || 3 || 4 || 5": - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.2.1, semver@^7.3.4, semver@^7.3.5: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -2157,10 +2311,10 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" + integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== slash@^3.0.0: version "3.0.0" @@ -2176,7 +2330,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -2221,6 +2375,15 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string.prototype.trim@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" + integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string.prototype.trimend@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" @@ -2267,12 +2430,12 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== +strip-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853" + integrity sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA== dependencies: - min-indent "^1.0.0" + min-indent "^1.0.1" strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" @@ -2284,89 +2447,90 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== -stylelint-config-recommended-scss@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-8.0.0.tgz#1c1e93e619fe2275d4c1067928d92e0614f7d64f" - integrity sha512-BxjxEzRaZoQb7Iinc3p92GS6zRdRAkIuEu2ZFLTxJK2e1AIcCb5B5MXY9KOXdGTnYFZ+KKx6R4Fv9zU6CtMYPQ== +stylelint-config-recommended-scss@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-12.0.0.tgz#9d9e82c46012649f11bfebcbc788f58e61860f33" + integrity sha512-5Bb2mlGy6WLa30oNeKpZvavv2lowJUsUJO25+OA68GFTemlwd1zbFsL7q0bReKipOSU3sG47hKneZ6Nd+ctrFA== dependencies: - postcss-scss "^4.0.2" - stylelint-config-recommended "^9.0.0" - stylelint-scss "^4.0.0" + postcss-scss "^4.0.6" + stylelint-config-recommended "^12.0.0" + stylelint-scss "^5.0.0" -stylelint-config-recommended@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-9.0.0.tgz#1c9e07536a8cd875405f8ecef7314916d94e7e40" - integrity sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ== +stylelint-config-recommended@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-12.0.0.tgz#d0993232fca017065fd5acfcb52dd8a188784ef4" + integrity sha512-x6x8QNARrGO2sG6iURkzqL+Dp+4bJorPMMRNPScdvaUK8PsynriOcMW7AFDKqkWAS5wbue/u8fUT/4ynzcmqdQ== -stylelint-config-standard-scss@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/stylelint-config-standard-scss/-/stylelint-config-standard-scss-6.1.0.tgz#a6cddd2a9430578b92fc89726a59474d5548a444" - integrity sha512-iZ2B5kQT2G3rUzx+437cEpdcnFOQkwnwqXuY8Z0QUwIHQVE8mnYChGAquyKFUKZRZ0pRnrciARlPaR1RBtPb0Q== +stylelint-config-standard-scss@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard-scss/-/stylelint-config-standard-scss-10.0.0.tgz#159a54a01b80649bf0143fa7ba086b676a1a749e" + integrity sha512-bChBEo1p3xUVWh/wenJI+josoMk21f2yuLDGzGjmKYcALfl2u3DFltY+n4UHswYiXghqXaA8mRh+bFy/q1hQlg== dependencies: - stylelint-config-recommended-scss "^8.0.0" - stylelint-config-standard "^29.0.0" + stylelint-config-recommended-scss "^12.0.0" + stylelint-config-standard "^33.0.0" -stylelint-config-standard@^29.0.0: - version "29.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-29.0.0.tgz#4cc0e0f05512a39bb8b8e97853247d3a95d66fa2" - integrity sha512-uy8tZLbfq6ZrXy4JKu3W+7lYLgRQBxYTUUB88vPgQ+ZzAxdrvcaSUW9hOMNLYBnwH+9Kkj19M2DHdZ4gKwI7tg== +stylelint-config-standard@^33.0.0: + version "33.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-33.0.0.tgz#1f7bb299153a53874073e93829e37a475842f0f9" + integrity sha512-eyxnLWoXImUn77+ODIuW9qXBDNM+ALN68L3wT1lN2oNspZ7D9NVGlNHb2QCUn4xDug6VZLsh0tF8NyoYzkgTzg== dependencies: - stylelint-config-recommended "^9.0.0" + stylelint-config-recommended "^12.0.0" -stylelint-scss@^4.0.0, stylelint-scss@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-4.4.0.tgz#87ce9d049eff1ce67cce788780fbfda63099017e" - integrity sha512-Qy66a+/30aylFhPmUArHhVsHOun1qrO93LGT15uzLuLjWS7hKDfpFm34mYo1ndR4MCo8W4bEZM1+AlJRJORaaw== +stylelint-scss@^5.0.0, stylelint-scss@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-5.0.1.tgz#b33a6580b5734eace083cfc2cc3021225e28547f" + integrity sha512-n87iCRZrr2J7//I/QFsDXxFLnHKw633U4qvWZ+mOW6KDAp/HLj06H+6+f9zOuTYy+MdGdTuCSDROCpQIhw5fvQ== dependencies: - lodash "^4.17.21" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" - postcss-selector-parser "^6.0.6" - postcss-value-parser "^4.1.0" + postcss-selector-parser "^6.0.13" + postcss-value-parser "^4.2.0" -stylelint@^14.16.1: - version "14.16.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-14.16.1.tgz#b911063530619a1bbe44c2b875fd8181ebdc742d" - integrity sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A== +stylelint@^15.10.2: + version "15.10.2" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.2.tgz#0ee5a8371d3a2e1ff27fefd48309d3ddef7c3405" + integrity sha512-UxqSb3hB74g4DTO45QhUHkJMjKKU//lNUAOWyvPBVPZbCknJ5HjOWWZo+UDuhHa9FLeVdHBZXxu43eXkjyIPWg== dependencies: - "@csstools/selector-specificity" "^2.0.2" + "@csstools/css-parser-algorithms" "^2.3.0" + "@csstools/css-tokenizer" "^2.1.1" + "@csstools/media-query-list-parser" "^2.1.2" + "@csstools/selector-specificity" "^3.0.0" balanced-match "^2.0.0" colord "^2.9.3" - cosmiconfig "^7.1.0" - css-functions-list "^3.1.0" + cosmiconfig "^8.2.0" + css-functions-list "^3.2.0" + css-tree "^2.3.1" debug "^4.3.4" - fast-glob "^3.2.12" + fast-glob "^3.3.0" fastest-levenshtein "^1.0.16" file-entry-cache "^6.0.1" global-modules "^2.0.0" globby "^11.1.0" globjoin "^0.1.4" - html-tags "^3.2.0" - ignore "^5.2.1" + html-tags "^3.3.1" + ignore "^5.2.4" import-lazy "^4.0.0" imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.26.0" + known-css-properties "^0.27.0" mathml-tag-names "^2.1.3" - meow "^9.0.0" + meow "^10.1.5" micromatch "^4.0.5" normalize-path "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.19" - postcss-media-query-parser "^0.2.3" + postcss "^8.4.25" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^6.0.0" - postcss-selector-parser "^6.0.11" + postcss-selector-parser "^6.0.13" postcss-value-parser "^4.2.0" resolve-from "^5.0.0" string-width "^4.2.3" strip-ansi "^6.0.1" style-search "^0.1.0" - supports-hyperlinks "^2.3.0" + supports-hyperlinks "^3.0.0" svg-tags "^1.0.0" table "^6.8.1" - v8-compile-cache "^2.3.0" - write-file-atomic "^4.0.2" + write-file-atomic "^5.0.1" supports-color@^5.3.0: version "5.5.0" @@ -2382,10 +2546,10 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-hyperlinks@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" - integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== +supports-hyperlinks@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b" + integrity sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA== dependencies: has-flag "^4.0.0" supports-color "^7.0.0" @@ -2405,18 +2569,7 @@ sweetalert@1.1.3: resolved "https://registry.yarnpkg.com/sweetalert/-/sweetalert-1.1.3.tgz#d2c31ea492b22b6a8d887aea15989a238fc084ae" integrity sha1-0sMepJKyK2qNiHrqFZiaI4/AhK4= -table@^6.0.9: - version "6.8.0" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" - integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - -table@^6.8.1: +table@^6.0.9, table@^6.8.1: version "6.8.1" resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== @@ -2444,18 +2597,18 @@ toastify-js@^1.12.0: resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.12.0.tgz#cc1c4f5c7e7380e854e20bedceb51980ea29f64d" integrity sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ== -trim-newlines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" - integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +trim-newlines@^4.0.2: + version "4.1.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" + integrity sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ== -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== +tsconfig-paths@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" + integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== dependencies: "@types/json5" "^0.0.29" - json5 "^1.0.1" + json5 "^1.0.2" minimist "^1.2.6" strip-bom "^3.0.0" @@ -2478,25 +2631,45 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-fest@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" - integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== +type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" typed-array-length@^1.0.4: version "1.0.4" @@ -2507,10 +2680,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== unbox-primitive@^1.0.1: version "1.0.1" @@ -2544,7 +2717,7 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: +v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== @@ -2568,6 +2741,17 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-typed-array@^1.1.10: + version "1.1.11" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" + integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== + 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" + 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" @@ -2595,34 +2779,34 @@ which@^2.0.1: isexe "^2.0.0" word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== +write-file-atomic@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" + integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== dependencies: imurmurhash "^0.1.4" - signal-exit "^3.0.7" + signal-exit "^4.0.1" yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@^20.2.3: +yargs-parser@^20.2.9: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +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==