th: Merge remote-tracking branch 'glitch/main' (18eacc7a07
)
This commit is contained in:
commit
b24d7bb6f2
460 changed files with 8249 additions and 6749 deletions
|
@ -70,7 +70,7 @@ services:
|
||||||
hard: -1
|
hard: -1
|
||||||
|
|
||||||
libretranslate:
|
libretranslate:
|
||||||
image: libretranslate/libretranslate:v1.3.11
|
image: libretranslate/libretranslate:v1.3.12
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- lt-data:/home/libretranslate/.local
|
- lt-data:/home/libretranslate/.local
|
||||||
|
|
|
@ -9,7 +9,6 @@ module.exports = {
|
||||||
'plugin:import/recommended',
|
'plugin:import/recommended',
|
||||||
'plugin:promise/recommended',
|
'plugin:promise/recommended',
|
||||||
'plugin:jsdoc/recommended',
|
'plugin:jsdoc/recommended',
|
||||||
'plugin:prettier/recommended',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
|
@ -63,7 +62,9 @@ module.exports = {
|
||||||
'consistent-return': 'error',
|
'consistent-return': 'error',
|
||||||
'dot-notation': 'error',
|
'dot-notation': 'error',
|
||||||
eqeqeq: ['error', 'always', { 'null': 'ignore' }],
|
eqeqeq: ['error', 'always', { 'null': 'ignore' }],
|
||||||
|
'indent': ['error', 2],
|
||||||
'jsx-quotes': ['error', 'prefer-single'],
|
'jsx-quotes': ['error', 'prefer-single'],
|
||||||
|
'semi': ['error', 'always'],
|
||||||
'no-case-declarations': 'off',
|
'no-case-declarations': 'off',
|
||||||
'no-catch-shadow': 'error',
|
'no-catch-shadow': 'error',
|
||||||
'no-console': [
|
'no-console': [
|
||||||
|
|
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
|
@ -1,3 +0,0 @@
|
||||||
patreon: mastodon
|
|
||||||
open_collective: mastodon
|
|
||||||
custom: https://sponsor.joinmastodon.org
|
|
14
.github/workflows/build-container-image.yml
vendored
14
.github/workflows/build-container-image.yml
vendored
|
@ -29,10 +29,10 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v2
|
- uses: docker/setup-qemu-action@v3
|
||||||
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v2
|
- uses: docker/setup-buildx-action@v3
|
||||||
id: buildx
|
id: buildx
|
||||||
if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }}
|
if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
|
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v2
|
- uses: docker/setup-buildx-action@v3
|
||||||
id: buildx-native
|
id: buildx-native
|
||||||
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
||||||
with:
|
with:
|
||||||
|
@ -61,20 +61,20 @@ jobs:
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: contains(inputs.push_to_images, 'tootsuite')
|
if: contains(inputs.push_to_images, 'tootsuite')
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to the Github Container registry
|
- name: Log in to the Github Container registry
|
||||||
if: contains(inputs.push_to_images, 'ghcr.io')
|
if: contains(inputs.push_to_images, 'ghcr.io')
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: docker/metadata-action@v4
|
- uses: docker/metadata-action@v5
|
||||||
id: meta
|
id: meta
|
||||||
if: ${{ inputs.push_to_images != '' }}
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
with:
|
with:
|
||||||
|
@ -83,7 +83,7 @@ jobs:
|
||||||
tags: ${{ inputs.tags }}
|
tags: ${{ inputs.tags }}
|
||||||
labels: ${{ inputs.labels }}
|
labels: ${{ inputs.labels }}
|
||||||
|
|
||||||
- uses: docker/build-push-action@v4
|
- uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
build-args: |
|
build-args: |
|
||||||
|
|
1
.github/workflows/build-nightly.yml
vendored
1
.github/workflows/build-nightly.yml
vendored
|
@ -11,6 +11,7 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
compute-suffix:
|
compute-suffix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'glitch-soc/mastodon'
|
||||||
steps:
|
steps:
|
||||||
- id: version_vars
|
- id: version_vars
|
||||||
env:
|
env:
|
||||||
|
|
2
.github/workflows/build-releases.yml
vendored
2
.github/workflows/build-releases.yml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
||||||
# Only tag with latest when ran against the latest stable branch
|
# Only tag with latest when ran against the latest stable branch
|
||||||
# This needs to be updated after each minor version release
|
# This needs to be updated after each minor version release
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
|
latest=${{ startsWith(github.ref, 'refs/tags/v4.2.') }}
|
||||||
tags: |
|
tags: |
|
||||||
type=pep440,pattern={{raw}}
|
type=pep440,pattern={{raw}}
|
||||||
type=pep440,pattern=v{{major}}.{{minor}}
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
|
|
1
.github/workflows/crowdin-download.yml
vendored
1
.github/workflows/crowdin-download.yml
vendored
|
@ -11,6 +11,7 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
download-translations:
|
download-translations:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'glitch-soc/mastodon'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
10
.github/workflows/test-ruby.yml
vendored
10
.github/workflows/test-ruby.yml
vendored
|
@ -113,6 +113,7 @@ jobs:
|
||||||
CAS_ENABLED: true
|
CAS_ENABLED: true
|
||||||
BUNDLE_WITH: 'pam_authentication test'
|
BUNDLE_WITH: 'pam_authentication test'
|
||||||
CI_JOBS: ${{ matrix.ci_job }}/4
|
CI_JOBS: ${{ matrix.ci_job }}/4
|
||||||
|
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
@ -282,8 +283,8 @@ jobs:
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
|
|
||||||
elasticsearch:
|
search:
|
||||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
image: ${{ matrix.search-image }}
|
||||||
env:
|
env:
|
||||||
discovery.type: single-node
|
discovery.type: single-node
|
||||||
xpack.security.enabled: false
|
xpack.security.enabled: false
|
||||||
|
@ -313,6 +314,11 @@ jobs:
|
||||||
- '3.0'
|
- '3.0'
|
||||||
- '3.1'
|
- '3.1'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
search-image:
|
||||||
|
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
||||||
|
include:
|
||||||
|
- ruby-version: '.ruby-version'
|
||||||
|
search-image: docker.elastic.co/elasticsearch/elasticsearch:8.10.2
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -37,9 +37,6 @@
|
||||||
# Ignore Vagrant files
|
# Ignore Vagrant files
|
||||||
.vagrant/
|
.vagrant/
|
||||||
|
|
||||||
# Ignore Capistrano customizations
|
|
||||||
/config/deploy/*
|
|
||||||
|
|
||||||
# Ignore IDE files
|
# Ignore IDE files
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `haml-lint --auto-gen-config`
|
# `haml-lint --auto-gen-config`
|
||||||
# on 2023-07-20 09:47:50 -0400 using Haml-Lint version 0.48.0.
|
# on 2023-10-11 11:31:24 -0400 using Haml-Lint version 0.51.0.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the lints are removed from the code base.
|
# one by one as the lints are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
# versions of Haml-Lint, may require this file to be generated again.
|
# versions of Haml-Lint, may require this file to be generated again.
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# Offense count: 951
|
# Offense count: 946
|
||||||
LineLength:
|
LineLength:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ linters:
|
||||||
UnnecessaryStringOutput:
|
UnnecessaryStringOutput:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
# Offense count: 57
|
# Offense count: 44
|
||||||
RuboCop:
|
RuboCop:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
@ -26,22 +26,8 @@ linters:
|
||||||
- 'app/views/admin/reports/show.html.haml'
|
- 'app/views/admin/reports/show.html.haml'
|
||||||
- 'app/views/disputes/strikes/show.html.haml'
|
- 'app/views/disputes/strikes/show.html.haml'
|
||||||
|
|
||||||
# Offense count: 32
|
# Offense count: 2
|
||||||
InstanceVariables:
|
|
||||||
exclude:
|
|
||||||
- 'app/views/admin/reports/_actions.html.haml'
|
|
||||||
- 'app/views/admin/roles/_form.html.haml'
|
|
||||||
- 'app/views/admin/webhooks/_form.html.haml'
|
|
||||||
- 'app/views/auth/registrations/_status.html.haml'
|
|
||||||
- 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml'
|
|
||||||
- 'app/views/authorize_interactions/_post_follow_actions.html.haml'
|
|
||||||
- 'app/views/invites/_form.html.haml'
|
|
||||||
- 'app/views/relationships/_account.html.haml'
|
|
||||||
- 'app/views/shared/_og.html.haml'
|
|
||||||
|
|
||||||
# Offense count: 3
|
|
||||||
IdNames:
|
IdNames:
|
||||||
exclude:
|
exclude:
|
||||||
- 'app/views/authorize_interactions/error.html.haml'
|
|
||||||
- 'app/views/oauth/authorizations/error.html.haml'
|
- 'app/views/oauth/authorizations/error.html.haml'
|
||||||
- 'app/views/shared/_error_messages.html.haml'
|
- 'app/views/shared/_error_messages.html.haml'
|
||||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
20.7
|
20.8
|
||||||
|
|
|
@ -31,9 +31,6 @@
|
||||||
# Ignore Vagrant files
|
# Ignore Vagrant files
|
||||||
.vagrant/
|
.vagrant/
|
||||||
|
|
||||||
# Ignore Capistrano customizations
|
|
||||||
/config/deploy/*
|
|
||||||
|
|
||||||
# Ignore IDE files
|
# Ignore IDE files
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
@ -28,6 +28,7 @@ AllCops:
|
||||||
- 'Vagrantfile'
|
- 'Vagrantfile'
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
- 'lib/json_ld/*' # Generated files
|
- 'lib/json_ld/*' # Generated files
|
||||||
|
- 'lib/mastodon/migration_helpers.rb' # Vendored from GitLab
|
||||||
- 'lib/templates/**/*'
|
- 'lib/templates/**/*'
|
||||||
|
|
||||||
# Reason: Prefer Hashes without extreme indentation
|
# Reason: Prefer Hashes without extreme indentation
|
||||||
|
@ -75,12 +76,6 @@ Metrics/AbcSize:
|
||||||
- 'lib/mastodon/cli/*.rb'
|
- 'lib/mastodon/cli/*.rb'
|
||||||
- db/*migrate/**/*
|
- db/*migrate/**/*
|
||||||
|
|
||||||
# Reason:
|
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
|
|
||||||
Metrics/BlockNesting:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/mastodon/cli/*.rb'
|
|
||||||
|
|
||||||
# Reason: Currently disabled in .rubocop_todo.yml
|
# Reason: Currently disabled in .rubocop_todo.yml
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
|
|
|
@ -13,32 +13,6 @@ Bundler/OrderedGems:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'Gemfile'
|
- 'Gemfile'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
|
||||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
||||||
# SupportedStyles: with_first_argument, with_fixed_indentation
|
|
||||||
Layout/ArgumentAlignment:
|
|
||||||
Exclude:
|
|
||||||
- 'config/initializers/cors.rb'
|
|
||||||
- 'config/initializers/session_store.rb'
|
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
|
||||||
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
|
||||||
# SupportedHashRocketStyles: key, separator, table
|
|
||||||
# SupportedColonStyles: key, separator, table
|
|
||||||
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
|
||||||
Layout/HashAlignment:
|
|
||||||
Exclude:
|
|
||||||
- 'config/environments/production.rb'
|
|
||||||
- 'config/initializers/rack_attack.rb'
|
|
||||||
- 'config/routes.rb'
|
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
|
||||||
# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment.
|
|
||||||
Layout/LeadingCommentSpace:
|
|
||||||
Exclude:
|
|
||||||
- 'config/application.rb'
|
|
||||||
- 'config/initializers/3_omniauth.rb'
|
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
|
||||||
# URISchemes: http, https
|
# URISchemes: http, https
|
||||||
|
@ -46,14 +20,6 @@ Layout/LineLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/account.rb'
|
- 'app/models/account.rb'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: require_no_space, require_space
|
|
||||||
Layout/SpaceInLambdaLiteral:
|
|
||||||
Exclude:
|
|
||||||
- 'config/environments/production.rb'
|
|
||||||
- 'config/initializers/content_security_policy.rb'
|
|
||||||
|
|
||||||
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
||||||
Lint/EmptyBlock:
|
Lint/EmptyBlock:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -82,27 +48,6 @@ Lint/UnusedBlockArgument:
|
||||||
- 'config/initializers/paperclip.rb'
|
- 'config/initializers/paperclip.rb'
|
||||||
- 'config/initializers/simple_form.rb'
|
- 'config/initializers/simple_form.rb'
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
||||||
Lint/UselessAssignment:
|
|
||||||
Exclude:
|
|
||||||
- 'app/services/activitypub/process_status_update_service.rb'
|
|
||||||
- 'config/initializers/3_omniauth.rb'
|
|
||||||
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
|
|
||||||
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
|
|
||||||
- 'spec/controllers/api/v1/favourites_controller_spec.rb'
|
|
||||||
- 'spec/controllers/concerns/account_controller_concern_spec.rb'
|
|
||||||
- 'spec/helpers/jsonld_helper_spec.rb'
|
|
||||||
- 'spec/models/account_spec.rb'
|
|
||||||
- 'spec/models/domain_block_spec.rb'
|
|
||||||
- 'spec/models/status_spec.rb'
|
|
||||||
- 'spec/models/user_spec.rb'
|
|
||||||
- 'spec/models/webauthn_credentials_spec.rb'
|
|
||||||
- 'spec/services/account_search_service_spec.rb'
|
|
||||||
- 'spec/services/post_status_service_spec.rb'
|
|
||||||
- 'spec/services/precompute_feed_service_spec.rb'
|
|
||||||
- 'spec/services/resolve_url_service_spec.rb'
|
|
||||||
- 'spec/views/statuses/show.html.haml_spec.rb'
|
|
||||||
|
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 144
|
Max: 144
|
||||||
|
@ -122,26 +67,6 @@ Metrics/CyclomaticComplexity:
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 27
|
Max: 27
|
||||||
|
|
||||||
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
|
||||||
# SupportedStyles: snake_case, normalcase, non_integer
|
|
||||||
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
|
||||||
Naming/VariableNumber:
|
|
||||||
Exclude:
|
|
||||||
- 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
|
|
||||||
- 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
|
|
||||||
- 'db/migrate/20190820003045_update_statuses_index.rb'
|
|
||||||
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
|
|
||||||
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
|
|
||||||
- 'spec/models/account_spec.rb'
|
|
||||||
- 'spec/models/domain_block_spec.rb'
|
|
||||||
- 'spec/models/user_spec.rb'
|
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
||||||
# Configuration parameters: SafeMultiline.
|
|
||||||
Performance/DeletePrefix:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/featured_tag.rb'
|
|
||||||
|
|
||||||
Performance/MapMethodChain:
|
Performance/MapMethodChain:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/feed.rb'
|
- 'app/models/feed.rb'
|
||||||
|
@ -214,9 +139,7 @@ RSpec/LetSetup:
|
||||||
- 'spec/controllers/admin/reports/actions_controller_spec.rb'
|
- 'spec/controllers/admin/reports/actions_controller_spec.rb'
|
||||||
- 'spec/controllers/admin/statuses_controller_spec.rb'
|
- 'spec/controllers/admin/statuses_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
|
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
|
|
||||||
- 'spec/controllers/api/v1/filters_controller_spec.rb'
|
- 'spec/controllers/api/v1/filters_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v1/followed_tags_controller_spec.rb'
|
|
||||||
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
|
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
|
- 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
|
||||||
- 'spec/controllers/api/v2/filters/statuses_controller_spec.rb'
|
- 'spec/controllers/api/v2/filters/statuses_controller_spec.rb'
|
||||||
|
@ -291,10 +214,6 @@ RSpec/MultipleMemoizedHelpers:
|
||||||
RSpec/NestedGroups:
|
RSpec/NestedGroups:
|
||||||
Max: 6
|
Max: 6
|
||||||
|
|
||||||
RSpec/PendingWithoutReason:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/models/account_spec.rb'
|
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -456,7 +375,6 @@ Rails/SkipsModelValidations:
|
||||||
- 'lib/mastodon/cli/accounts.rb'
|
- 'lib/mastodon/cli/accounts.rb'
|
||||||
- 'lib/mastodon/cli/main.rb'
|
- 'lib/mastodon/cli/main.rb'
|
||||||
- 'lib/mastodon/cli/maintenance.rb'
|
- 'lib/mastodon/cli/maintenance.rb'
|
||||||
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
|
|
||||||
- 'spec/lib/activitypub/activity/follow_spec.rb'
|
- 'spec/lib/activitypub/activity/follow_spec.rb'
|
||||||
- 'spec/services/follow_service_spec.rb'
|
- 'spec/services/follow_service_spec.rb'
|
||||||
- 'spec/services/update_account_service_spec.rb'
|
- 'spec/services/update_account_service_spec.rb'
|
||||||
|
@ -561,12 +479,6 @@ Style/ClassVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/devise.rb'
|
- 'config/initializers/devise.rb'
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
||||||
Style/CombinableLoops:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/form/custom_emoji_batch.rb'
|
|
||||||
- 'app/models/form/ip_block_batch.rb'
|
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: AllowedVars.
|
# Configuration parameters: AllowedVars.
|
||||||
Style/FetchEnvVar:
|
Style/FetchEnvVar:
|
||||||
|
@ -844,6 +756,5 @@ Style/TrailingCommaInHashLiteral:
|
||||||
Style/WordArray:
|
Style/WordArray:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/helpers/languages_helper.rb'
|
- 'app/helpers/languages_helper.rb'
|
||||||
- 'config/initializers/cors.rb'
|
|
||||||
- 'spec/controllers/settings/imports_controller_spec.rb'
|
- 'spec/controllers/settings/imports_controller_spec.rb'
|
||||||
- 'spec/models/form/import_spec.rb'
|
- 'spec/models/form/import_spec.rb'
|
||||||
|
|
1680
AUTHORS.md
1680
AUTHORS.md
File diff suppressed because it is too large
Load diff
57
CHANGELOG.md
57
CHANGELOG.md
|
@ -2,13 +2,53 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [4.2.0] - UNRELEASED
|
## [4.2.1] - 2023-10-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add redirection on `/deck` URLs for logged-out users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27128))
|
||||||
|
- Add support for v4.2.0 migrations to `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27147))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change some worker lock TTLs to be shorter-lived ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27246))
|
||||||
|
- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix duplicate reports being sent when reporting some remote posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27355))
|
||||||
|
- Fix clicking on already-opened thread post scrolling to the top of the thread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27331), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27338), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27350))
|
||||||
|
- Fix some remote posts getting truncated ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27307))
|
||||||
|
- Fix some cases of infinite scroll code trying to fetch inaccessible posts in a loop ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27286))
|
||||||
|
- Fix `Vary` headers not being set on some redirects ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27272))
|
||||||
|
- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656))
|
||||||
|
- Fix unexpected linebreak in version string in the Web UI ([vmstan](https://github.com/mastodon/mastodon/pull/26986))
|
||||||
|
- Fix double scroll bars in some columns in advanced interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27187))
|
||||||
|
- Fix boosts of local users being filtered in account timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27204))
|
||||||
|
- Fix multiple instances of the trend refresh scheduler sometimes running at once ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27253))
|
||||||
|
- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258))
|
||||||
|
- Fix incorrectly keeping outdated update notices absent from the API endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27021))
|
||||||
|
- Fix import progress not updating on certain failures ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27247))
|
||||||
|
- Fix websocket connections being incorrectly decremented twice on errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/27238))
|
||||||
|
- Fix explore prompt appearing because of posts being received out of order ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27211))
|
||||||
|
- Fix explore prompt sometimes showing up when the home TL is loading ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27062))
|
||||||
|
- Fix link handling of mentions in user profiles when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27185))
|
||||||
|
- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186))
|
||||||
|
- Fix notification toasts not respecting reduce-motion ([c960657](https://github.com/mastodon/mastodon/pull/27178))
|
||||||
|
- Fix retention dashboard not displaying correct month ([vmstan](https://github.com/mastodon/mastodon/pull/27180))
|
||||||
|
- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111))
|
||||||
|
- Fix division by zero in video in bitrate computation code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27129))
|
||||||
|
- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306))
|
||||||
|
- Fix ActiveRecord using two connection pools when no replica is defined ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27061))
|
||||||
|
- Fix the search documentation URL in system checks ([renchap](https://github.com/mastodon/mastodon/pull/27036))
|
||||||
|
|
||||||
|
## [4.2.0] - 2023-09-21
|
||||||
|
|
||||||
The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by [@danielmbrasil](https://github.com/danielmbrasil), [@mjankowski](https://github.com/mjankowski), [@nschonni](https://github.com/nschonni), [@renchap](https://github.com/renchap), and [@takayamaki](https://github.com/takayamaki).
|
The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by [@danielmbrasil](https://github.com/danielmbrasil), [@mjankowski](https://github.com/mjankowski), [@nschonni](https://github.com/nschonni), [@renchap](https://github.com/renchap), and [@takayamaki](https://github.com/takayamaki).
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Add full-text search of opted-in public posts and rework search operators** ([Gargron](https://github.com/mastodon/mastodon/pull/26485), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26344), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26657), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26650), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26659), [Gargron](https://github.com/mastodon/mastodon/pull/26660), [Gargron](https://github.com/mastodon/mastodon/pull/26663), [Gargron](https://github.com/mastodon/mastodon/pull/26688), [Gargron](https://github.com/mastodon/mastodon/pull/26689), [Gargron](https://github.com/mastodon/mastodon/pull/26686), [Gargron](https://github.com/mastodon/mastodon/pull/26687), [Gargron](https://github.com/mastodon/mastodon/pull/26692), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26697), [Gargron](https://github.com/mastodon/mastodon/pull/26699), [Gargron](https://github.com/mastodon/mastodon/pull/26701), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26710), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26739), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26754), [Gargron](https://github.com/mastodon/mastodon/pull/26662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26755), [Gargron](https://github.com/mastodon/mastodon/pull/26781), [Gargron](https://github.com/mastodon/mastodon/pull/26782), [Gargron](https://github.com/mastodon/mastodon/pull/26760), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26756), [Gargron](https://github.com/mastodon/mastodon/pull/26784), [Gargron](https://github.com/mastodon/mastodon/pull/26807), [Gargron](https://github.com/mastodon/mastodon/pull/26835), [Gargron](https://github.com/mastodon/mastodon/pull/26847), [Gargron](https://github.com/mastodon/mastodon/pull/26834), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26893), [tribela](https://github.com/mastodon/mastodon/pull/26896), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26927), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26959))
|
- **Add full-text search of opted-in public posts and rework search operators** ([Gargron](https://github.com/mastodon/mastodon/pull/26485), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26344), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26657), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26650), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26659), [Gargron](https://github.com/mastodon/mastodon/pull/26660), [Gargron](https://github.com/mastodon/mastodon/pull/26663), [Gargron](https://github.com/mastodon/mastodon/pull/26688), [Gargron](https://github.com/mastodon/mastodon/pull/26689), [Gargron](https://github.com/mastodon/mastodon/pull/26686), [Gargron](https://github.com/mastodon/mastodon/pull/26687), [Gargron](https://github.com/mastodon/mastodon/pull/26692), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26697), [Gargron](https://github.com/mastodon/mastodon/pull/26699), [Gargron](https://github.com/mastodon/mastodon/pull/26701), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26710), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26739), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26754), [Gargron](https://github.com/mastodon/mastodon/pull/26662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26755), [Gargron](https://github.com/mastodon/mastodon/pull/26781), [Gargron](https://github.com/mastodon/mastodon/pull/26782), [Gargron](https://github.com/mastodon/mastodon/pull/26760), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26756), [Gargron](https://github.com/mastodon/mastodon/pull/26784), [Gargron](https://github.com/mastodon/mastodon/pull/26807), [Gargron](https://github.com/mastodon/mastodon/pull/26835), [Gargron](https://github.com/mastodon/mastodon/pull/26847), [Gargron](https://github.com/mastodon/mastodon/pull/26834), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26893), [tribela](https://github.com/mastodon/mastodon/pull/26896), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26927), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27014))
|
||||||
This introduces a new `public_statuses` Elasticsearch index for public posts by users who have opted in to their posts being searchable (`toot#indexable` flag).
|
This introduces a new `public_statuses` Elasticsearch index for public posts by users who have opted in to their posts being searchable (`toot#indexable` flag).
|
||||||
This also revisits the other indexes to provide more useful indexing, and adds new search operators such as `from:me`, `before:2022-11-01`, `after:2022-11-01`, `during:2022-11-01`, `language:fr`, `has:poll`, or `in:library` (for searching only in posts you have written or interacted with).
|
This also revisits the other indexes to provide more useful indexing, and adds new search operators such as `from:me`, `before:2022-11-01`, `after:2022-11-01`, `during:2022-11-01`, `language:fr`, `has:poll`, or `in:library` (for searching only in posts you have written or interacted with).
|
||||||
Results are now ordered chronologically.
|
Results are now ordered chronologically.
|
||||||
|
@ -27,15 +67,16 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- Add `ONE_CLICK_SSO_LOGIN` environment variable to directly link to the Single-Sign On provider if there is only one sign up method available ([CSDUMMI](https://github.com/mastodon/mastodon/pull/26083), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26368), [CSDUMMI](https://github.com/mastodon/mastodon/pull/26857), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26901))
|
- Add `ONE_CLICK_SSO_LOGIN` environment variable to directly link to the Single-Sign On provider if there is only one sign up method available ([CSDUMMI](https://github.com/mastodon/mastodon/pull/26083), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26368), [CSDUMMI](https://github.com/mastodon/mastodon/pull/26857), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26901))
|
||||||
- **Add webhook templating** ([Gargron](https://github.com/mastodon/mastodon/pull/23289))
|
- **Add webhook templating** ([Gargron](https://github.com/mastodon/mastodon/pull/23289))
|
||||||
- **Add webhooks for local `status.created`, `status.updated`, `account.updated` and `report.updated`** ([VyrCossont](https://github.com/mastodon/mastodon/pull/24133), [VyrCossont](https://github.com/mastodon/mastodon/pull/24243), [VyrCossont](https://github.com/mastodon/mastodon/pull/24211))
|
- **Add webhooks for local `status.created`, `status.updated`, `account.updated` and `report.updated`** ([VyrCossont](https://github.com/mastodon/mastodon/pull/24133), [VyrCossont](https://github.com/mastodon/mastodon/pull/24243), [VyrCossont](https://github.com/mastodon/mastodon/pull/24211))
|
||||||
- **Add exclusive lists** ([dariusk](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324))
|
- **Add exclusive lists** ([dariusk, necropolina](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324))
|
||||||
- **Add a confirmation screen when suspending a domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25144), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25603))
|
- **Add a confirmation screen when suspending a domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25144), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25603))
|
||||||
- **Add support for importing lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25203), [mgmn](https://github.com/mastodon/mastodon/pull/26120), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26372))
|
- **Add support for importing lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25203), [mgmn](https://github.com/mastodon/mastodon/pull/26120), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26372))
|
||||||
- **Add optional hCaptcha support** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25019), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25057), [Gargron](https://github.com/mastodon/mastodon/pull/25395), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26388))
|
- **Add optional hCaptcha support** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25019), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25057), [Gargron](https://github.com/mastodon/mastodon/pull/25395), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26388))
|
||||||
- **Add lines to threads in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24549), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24677), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24696), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24711), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24713), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24715), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24800), [teeerevor](https://github.com/mastodon/mastodon/pull/25706), [renchap](https://github.com/mastodon/mastodon/pull/25807))
|
- **Add lines to threads in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24549), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24677), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24696), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24711), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24713), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24715), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24800), [teeerevor](https://github.com/mastodon/mastodon/pull/25706), [renchap](https://github.com/mastodon/mastodon/pull/25807))
|
||||||
- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561))
|
- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561))
|
||||||
- **Add `S3_DISABLE_CHECKSUM_MODE` environment variable for compatibility with some S3-compatible providers** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435))
|
|
||||||
- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510))
|
- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510))
|
||||||
- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26658))
|
- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26658))
|
||||||
|
- Add `hide_collections`, `discoverable` and `indexable` attributes to credentials API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26998))
|
||||||
|
- Add `S3_ENABLE_CHECKSUM_MODE` environment variable to enable checksum verification on compatible S3-providers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435))
|
||||||
- Add admin API for managing tags ([rrgeorge](https://github.com/mastodon/mastodon/pull/26872))
|
- Add admin API for managing tags ([rrgeorge](https://github.com/mastodon/mastodon/pull/26872))
|
||||||
- Add a link to hashtag timelines from the Trending hashtags moderation interface ([gunchleoc](https://github.com/mastodon/mastodon/pull/26724))
|
- Add a link to hashtag timelines from the Trending hashtags moderation interface ([gunchleoc](https://github.com/mastodon/mastodon/pull/26724))
|
||||||
- Add timezone to datetimes in e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26822))
|
- Add timezone to datetimes in e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26822))
|
||||||
|
@ -53,7 +94,7 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542))
|
- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542))
|
||||||
- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295))
|
- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295))
|
||||||
- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443))
|
- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443))
|
||||||
- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26737))
|
- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26737), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26979))
|
||||||
- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300))
|
- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300))
|
||||||
- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155))
|
- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155))
|
||||||
- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149))
|
- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149))
|
||||||
|
@ -174,7 +215,7 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- Change account search in moderation interface to allow searching by username including the leading `@` ([HeitorMC](https://github.com/mastodon/mastodon/pull/24242))
|
- Change account search in moderation interface to allow searching by username including the leading `@` ([HeitorMC](https://github.com/mastodon/mastodon/pull/24242))
|
||||||
- Change all components to use the same error page in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24512))
|
- Change all components to use the same error page in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24512))
|
||||||
- Change search pop-out in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24305))
|
- Change search pop-out in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24305))
|
||||||
- Change user settings to be stored in a more optimal way ([Gargron](https://github.com/mastodon/mastodon/pull/23630), [c960657](https://github.com/mastodon/mastodon/pull/24321), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24558), [Gargron](https://github.com/mastodon/mastodon/pull/24761), [Gargron](https://github.com/mastodon/mastodon/pull/24783), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25508), [jsgoldstein](https://github.com/mastodon/mastodon/pull/25340), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26884))
|
- Change user settings to be stored in a more optimal way ([Gargron](https://github.com/mastodon/mastodon/pull/23630), [c960657](https://github.com/mastodon/mastodon/pull/24321), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24558), [Gargron](https://github.com/mastodon/mastodon/pull/24761), [Gargron](https://github.com/mastodon/mastodon/pull/24783), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25508), [jsgoldstein](https://github.com/mastodon/mastodon/pull/25340), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26884), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27012))
|
||||||
- Change media upload limits and remove client-side resizing ([Gargron](https://github.com/mastodon/mastodon/pull/23726))
|
- Change media upload limits and remove client-side resizing ([Gargron](https://github.com/mastodon/mastodon/pull/23726))
|
||||||
- Change design of account rows in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24247), [Gargron](https://github.com/mastodon/mastodon/pull/24343), [Gargron](https://github.com/mastodon/mastodon/pull/24956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25131))
|
- Change design of account rows in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24247), [Gargron](https://github.com/mastodon/mastodon/pull/24343), [Gargron](https://github.com/mastodon/mastodon/pull/24956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25131))
|
||||||
- Change log-out to use Single Logout when using external log-in through OIDC ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24020))
|
- Change log-out to use Single Logout when using external log-in through OIDC ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24020))
|
||||||
|
@ -306,8 +347,8 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Fix missing HTML sanitization in translation API (CVE-2023-42452)
|
- Fix missing HTML sanitization in translation API (CVE-2023-42452, [GHSA-2693-xr3m-jhqr](https://github.com/mastodon/mastodon/security/advisories/GHSA-2693-xr3m-jhqr))
|
||||||
- Fix incorrect domain name normalization (CVE-2023-42451)
|
- Fix incorrect domain name normalization (CVE-2023-42451, [GHSA-v3xf-c9qf-j667](https://github.com/mastodon/mastodon/security/advisories/GHSA-v3xf-c9qf-j667))
|
||||||
|
|
||||||
## [4.1.7] - 2023-09-05
|
## [4.1.7] - 2023-09-05
|
||||||
|
|
||||||
|
|
15
Capfile
15
Capfile
|
@ -1,15 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'capistrano/setup'
|
|
||||||
require 'capistrano/deploy'
|
|
||||||
require 'capistrano/scm/git'
|
|
||||||
|
|
||||||
install_plugin Capistrano::SCM::Git
|
|
||||||
|
|
||||||
require 'capistrano/rbenv'
|
|
||||||
require 'capistrano/bundler'
|
|
||||||
require 'capistrano/yarn'
|
|
||||||
require 'capistrano/rails/assets'
|
|
||||||
require 'capistrano/rails/migrations'
|
|
||||||
|
|
||||||
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
|
|
|
@ -1,6 +1,6 @@
|
||||||
# syntax=docker/dockerfile:1.4
|
# syntax=docker/dockerfile:1.4
|
||||||
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
|
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
|
||||||
ARG NODE_IMAGE="node:20.6-bookworm-slim"
|
ARG NODE_IMAGE="node:20.8-bookworm-slim"
|
||||||
ARG RUBY_IMAGE=ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim
|
ARG RUBY_IMAGE=ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim
|
||||||
|
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
|
|
9
Gemfile
9
Gemfile
|
@ -106,6 +106,9 @@ group :test do
|
||||||
# Used to split testing into chunks in CI
|
# Used to split testing into chunks in CI
|
||||||
gem 'rspec_chunked', '~> 0.6'
|
gem 'rspec_chunked', '~> 0.6'
|
||||||
|
|
||||||
|
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
|
||||||
|
gem 'rspec-github', '~> 2.4', require: false
|
||||||
|
|
||||||
# RSpec progress bar formatter
|
# RSpec progress bar formatter
|
||||||
gem 'fuubar', '~> 2.5'
|
gem 'fuubar', '~> 2.5'
|
||||||
|
|
||||||
|
@ -170,12 +173,6 @@ group :development do
|
||||||
# Linter CLI for HAML files
|
# Linter CLI for HAML files
|
||||||
gem 'haml_lint', require: false
|
gem 'haml_lint', require: false
|
||||||
|
|
||||||
# Deployment automation
|
|
||||||
gem 'capistrano', '~> 3.17'
|
|
||||||
gem 'capistrano-rails', '~> 1.6'
|
|
||||||
gem 'capistrano-rbenv', '~> 2.2'
|
|
||||||
gem 'capistrano-yarn', '~> 2.0'
|
|
||||||
|
|
||||||
# Validate missing i18n keys
|
# Validate missing i18n keys
|
||||||
gem 'i18n-tasks', '~> 1.0', require: false
|
gem 'i18n-tasks', '~> 1.0', require: false
|
||||||
end
|
end
|
||||||
|
|
96
Gemfile.lock
96
Gemfile.lock
|
@ -84,9 +84,9 @@ GEM
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
active_model_serializers (0.10.13)
|
active_model_serializers (0.10.14)
|
||||||
actionpack (>= 4.1, < 7.1)
|
actionpack (>= 4.1)
|
||||||
activemodel (>= 4.1, < 7.1)
|
activemodel (>= 4.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (7.0.8)
|
activejob (7.0.8)
|
||||||
|
@ -112,8 +112,6 @@ GEM
|
||||||
addressable (2.8.5)
|
addressable (2.8.5)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
aes_key_wrap (1.1.0)
|
aes_key_wrap (1.1.0)
|
||||||
airbrussh (1.4.1)
|
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
|
||||||
android_key_attestation (0.3.0)
|
android_key_attestation (0.3.0)
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
|
@ -148,12 +146,12 @@ GEM
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.1.1)
|
base64 (0.1.1)
|
||||||
bcrypt (3.1.18)
|
bcrypt (3.1.19)
|
||||||
better_errors (2.10.1)
|
better_errors (2.10.1)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
rouge (>= 1.0.0)
|
rouge (>= 1.0.0)
|
||||||
better_html (2.0.1)
|
better_html (2.0.2)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
ast (~> 2.0)
|
ast (~> 2.0)
|
||||||
|
@ -175,21 +173,6 @@ GEM
|
||||||
bundler-audit (0.9.1)
|
bundler-audit (0.9.1)
|
||||||
bundler (>= 1.2.0, < 3)
|
bundler (>= 1.2.0, < 3)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
capistrano (3.17.3)
|
|
||||||
airbrussh (>= 1.0.0)
|
|
||||||
i18n
|
|
||||||
rake (>= 10.0.0)
|
|
||||||
sshkit (>= 1.9.0)
|
|
||||||
capistrano-bundler (2.1.0)
|
|
||||||
capistrano (~> 3.1)
|
|
||||||
capistrano-rails (1.6.3)
|
|
||||||
capistrano (~> 3.1)
|
|
||||||
capistrano-bundler (>= 1.1, < 3)
|
|
||||||
capistrano-rbenv (2.2.0)
|
|
||||||
capistrano (~> 3.1)
|
|
||||||
sshkit (~> 1.3)
|
|
||||||
capistrano-yarn (2.0.2)
|
|
||||||
capistrano (~> 3.0)
|
|
||||||
capybara (3.39.2)
|
capybara (3.39.2)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
|
@ -227,17 +210,17 @@ GEM
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.3.3)
|
date (3.3.3)
|
||||||
debug_inspector (1.1.0)
|
debug_inspector (1.1.0)
|
||||||
devise (4.9.2)
|
devise (4.9.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (4.1.0)
|
devise-two-factor (4.1.1)
|
||||||
activesupport (< 7.1)
|
activesupport (~> 7.0)
|
||||||
attr_encrypted (>= 1.3, < 5, != 2)
|
attr_encrypted (>= 1.3, < 5, != 2)
|
||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (< 7.1)
|
railties (~> 7.0)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
devise_pam_authenticatable2 (9.2.0)
|
devise_pam_authenticatable2 (9.2.0)
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
|
@ -325,7 +308,7 @@ GEM
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (1.1.0)
|
globalid (1.1.0)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 5.0)
|
||||||
haml (6.1.2)
|
haml (6.2.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
|
@ -334,8 +317,8 @@ GEM
|
||||||
activesupport (>= 5.1)
|
activesupport (>= 5.1)
|
||||||
haml (>= 4.0.6)
|
haml (>= 4.0.6)
|
||||||
railties (>= 5.1)
|
railties (>= 5.1)
|
||||||
haml_lint (0.50.0)
|
haml_lint (0.51.0)
|
||||||
haml (>= 4.0, < 6.2)
|
haml (>= 4.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
rubocop (>= 1.0)
|
rubocop (>= 1.0)
|
||||||
|
@ -363,14 +346,14 @@ GEM
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.14.1)
|
i18n (1.14.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (1.0.12)
|
i18n-tasks (1.0.13)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
better_html (>= 1.0, < 3.0)
|
better_html (>= 1.0, < 3.0)
|
||||||
erubi
|
erubi
|
||||||
highline (>= 2.0.0)
|
highline (>= 2.0.0)
|
||||||
i18n
|
i18n
|
||||||
parser (>= 2.2.3.0)
|
parser (>= 3.2.2.1)
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
|
@ -430,12 +413,12 @@ GEM
|
||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
lograge (0.13.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.3)
|
loofah (2.21.4)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
|
@ -458,7 +441,7 @@ GEM
|
||||||
mime-types-data (3.2023.0808)
|
mime-types-data (3.2023.0808)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.4)
|
mini_portile2 (2.8.4)
|
||||||
minitest (5.19.0)
|
minitest (5.20.0)
|
||||||
msgpack (1.7.1)
|
msgpack (1.7.1)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
|
@ -474,11 +457,8 @@ GEM
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.1)
|
||||||
timeout
|
timeout
|
||||||
net-scp (4.0.0)
|
|
||||||
net-ssh (>= 2.6.5, < 8.0.0)
|
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.3.3)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.1.0)
|
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.15.4)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
|
@ -514,7 +494,7 @@ GEM
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.17)
|
ox (2.14.17)
|
||||||
parallel (1.23.0)
|
parallel (1.23.0)
|
||||||
parser (3.2.2.3)
|
parser (3.2.2.4)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
|
@ -534,7 +514,7 @@ GEM
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
private_address_check (0.5.0)
|
private_address_check (0.5.0)
|
||||||
public_suffix (5.0.3)
|
public_suffix (5.0.3)
|
||||||
puma (6.3.1)
|
puma (6.4.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -575,14 +555,14 @@ GEM
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
activesupport (>= 5.0.1.rc1)
|
activesupport (>= 5.0.1.rc1)
|
||||||
rails-dom-testing (2.1.1)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
rails-i18n (7.0.7)
|
rails-i18n (7.0.8)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
railties (7.0.8)
|
railties (7.0.8)
|
||||||
|
@ -604,10 +584,10 @@ GEM
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
redlock (1.3.2)
|
redlock (1.3.2)
|
||||||
redis (>= 3.0.0, < 6.0)
|
redis (>= 3.0.0, < 6.0)
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.8.2)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.0)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.6)
|
||||||
|
@ -623,6 +603,8 @@ GEM
|
||||||
rspec-expectations (3.12.3)
|
rspec-expectations (3.12.3)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
|
rspec-github (2.4.0)
|
||||||
|
rspec-core (~> 3.0)
|
||||||
rspec-mocks (3.12.5)
|
rspec-mocks (3.12.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
|
@ -641,12 +623,12 @@ GEM
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.12.1)
|
||||||
rspec_chunked (0.6)
|
rspec_chunked (0.6)
|
||||||
rubocop (1.56.3)
|
rubocop (1.57.1)
|
||||||
base64 (~> 0.1.1)
|
base64 (~> 0.1.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.2.3)
|
parser (>= 3.2.2.4)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
|
@ -655,11 +637,11 @@ GEM
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.29.0)
|
rubocop-ast (1.29.0)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.2.1.0)
|
||||||
rubocop-capybara (2.18.0)
|
rubocop-capybara (2.19.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.23.1)
|
rubocop-factory_bot (2.23.1)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.33)
|
||||||
rubocop-performance (1.19.0)
|
rubocop-performance (1.19.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-ast (>= 0.4.0)
|
||||||
rubocop-rails (2.20.2)
|
rubocop-rails (2.20.2)
|
||||||
|
@ -687,12 +669,12 @@ GEM
|
||||||
scenic (1.7.0)
|
scenic (1.7.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
selenium-webdriver (4.11.0)
|
selenium-webdriver (4.13.1)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 3.0)
|
rubyzip (>= 1.2.2, < 3.0)
|
||||||
websocket (~> 1.0)
|
websocket (~> 1.0)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sidekiq (6.5.9)
|
sidekiq (6.5.12)
|
||||||
connection_pool (>= 2.2.5, < 3)
|
connection_pool (>= 2.2.5, < 3)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.5.0, < 5)
|
redis (>= 4.5.0, < 5)
|
||||||
|
@ -727,9 +709,6 @@ GEM
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sshkit (1.21.5)
|
|
||||||
net-scp (>= 1.1.2)
|
|
||||||
net-ssh (>= 2.8.0)
|
|
||||||
stackprof (0.2.25)
|
stackprof (0.2.25)
|
||||||
statsd-ruby (1.5.0)
|
statsd-ruby (1.5.0)
|
||||||
stoplight (3.0.2)
|
stoplight (3.0.2)
|
||||||
|
@ -748,7 +727,7 @@ GEM
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
test-prof (1.2.3)
|
test-prof (1.2.3)
|
||||||
thor (1.2.2)
|
thor (1.2.2)
|
||||||
tilt (2.2.0)
|
tilt (2.3.0)
|
||||||
timeout (0.4.0)
|
timeout (0.4.0)
|
||||||
tpm-key_attestation (0.12.0)
|
tpm-key_attestation (0.12.0)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
|
@ -774,7 +753,7 @@ GEM
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.5.0)
|
||||||
uri (0.12.2)
|
uri (0.12.2)
|
||||||
validate_email (0.1.6)
|
validate_email (0.1.6)
|
||||||
activemodel (>= 3.0)
|
activemodel (>= 3.0)
|
||||||
|
@ -805,7 +784,7 @@ GEM
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
websocket (1.2.9)
|
websocket (1.2.10)
|
||||||
websocket-driver (0.7.6)
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
|
@ -813,7 +792,7 @@ GEM
|
||||||
xorcist (1.1.3)
|
xorcist (1.1.3)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.11)
|
zeitwerk (2.6.12)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -830,10 +809,6 @@ DEPENDENCIES
|
||||||
brakeman (~> 6.0)
|
brakeman (~> 6.0)
|
||||||
browser
|
browser
|
||||||
bundler-audit (~> 0.9)
|
bundler-audit (~> 0.9)
|
||||||
capistrano (~> 3.17)
|
|
||||||
capistrano-rails (~> 1.6)
|
|
||||||
capistrano-rbenv (~> 2.2)
|
|
||||||
capistrano-yarn (~> 2.0)
|
|
||||||
capybara (~> 3.39)
|
capybara (~> 3.39)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
chewy (~> 7.3)
|
chewy (~> 7.3)
|
||||||
|
@ -914,6 +889,7 @@ DEPENDENCIES
|
||||||
redis (~> 4.5)
|
redis (~> 4.5)
|
||||||
redis-namespace (~> 1.10)
|
redis-namespace (~> 1.10)
|
||||||
rqrcode (~> 2.2)
|
rqrcode (~> 2.2)
|
||||||
|
rspec-github (~> 2.4)
|
||||||
rspec-rails (~> 6.0)
|
rspec-rails (~> 6.0)
|
||||||
rspec-sidekiq (~> 4.0)
|
rspec-sidekiq (~> 4.0)
|
||||||
rspec_chunked (~> 0.6)
|
rspec_chunked (~> 0.6)
|
||||||
|
|
|
@ -15,6 +15,7 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ---------------- |
|
| ------- | ---------------- |
|
||||||
|
| 4.2.x | Yes |
|
||||||
| 4.1.x | Yes |
|
| 4.1.x | Yes |
|
||||||
| 4.0.x | Until 2023-10-31 |
|
| 4.0.x | Until 2023-10-31 |
|
||||||
| 3.5.x | Until 2023-12-31 |
|
| 3.5.x | Until 2023-12-31 |
|
||||||
|
|
|
@ -5,15 +5,7 @@ class AboutController < ApplicationController
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :set_instance_presenter
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Apps::CredentialsController < Api::BaseController
|
class Api::V1::Apps::CredentialsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read }
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
|
return doorkeeper_render_error unless valid_doorkeeper_token?
|
||||||
|
|
||||||
|
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key client_id scopes)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
before_action :set_sessions, only: [:edit, :update]
|
before_action :set_sessions, only: [:edit, :update]
|
||||||
before_action :set_strikes, only: [:edit, :update]
|
before_action :set_strikes, only: [:edit, :update]
|
||||||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
|
||||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||||
before_action :require_not_suspended!, only: [:update]
|
before_action :require_not_suspended!, only: [:update]
|
||||||
before_action :set_cache_headers, only: [:edit, :update]
|
before_action :set_cache_headers, only: [:edit, :update]
|
||||||
|
@ -112,10 +111,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
|
use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
|
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
include TwoFactorAuthenticationConcern
|
include TwoFactorAuthenticationConcern
|
||||||
|
|
||||||
before_action :set_instance_presenter, only: [:new]
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
content_security_policy only: :new do |p|
|
content_security_policy only: :new do |p|
|
||||||
|
@ -104,10 +103,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
use_pack 'auth'
|
use_pack 'auth'
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'lighter'
|
@body_classes = 'lighter'
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,17 +9,11 @@ module AccountControllerConcern
|
||||||
FOLLOW_PER_PAGE = 12
|
FOLLOW_PER_PAGE = 12
|
||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :set_instance_presenter
|
|
||||||
|
|
||||||
after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
|
after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_link_headers
|
def set_link_headers
|
||||||
response.headers['Link'] = LinkHeader.new(
|
response.headers['Link'] = LinkHeader.new(
|
||||||
[
|
[
|
||||||
|
|
|
@ -5,6 +5,7 @@ module TwoFactorAuthenticationConcern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||||
|
helper_method :webauthn_enabled?
|
||||||
end
|
end
|
||||||
|
|
||||||
def two_factor_enabled?
|
def two_factor_enabled?
|
||||||
|
@ -89,4 +90,10 @@ module TwoFactorAuthenticationConcern
|
||||||
|
|
||||||
set_locale { render :two_factor }
|
set_locale { render :two_factor }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def webauthn_enabled?
|
||||||
|
@webauthn_enabled
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,11 +4,11 @@ module WebAppControllerConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
prepend_before_action :redirect_unauthenticated_to_permalinks!
|
vary_by 'Accept, Accept-Language, Cookie'
|
||||||
|
|
||||||
|
before_action :redirect_unauthenticated_to_permalinks!
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
before_action :set_app_body_class
|
before_action :set_app_body_class
|
||||||
|
|
||||||
vary_by 'Accept, Accept-Language, Cookie'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def skip_csrf_meta_tags?
|
def skip_csrf_meta_tags?
|
||||||
|
@ -23,8 +23,10 @@ module WebAppControllerConcern
|
||||||
return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in
|
return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in
|
||||||
|
|
||||||
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
||||||
|
return if redirect_path.blank?
|
||||||
|
|
||||||
redirect_to(redirect_path) if redirect_path.present?
|
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||||
|
redirect_to(redirect_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
def set_pack
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
class FollowerAccountsController < ApplicationController
|
class FollowerAccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
include WebAppControllerConcern
|
|
||||||
|
|
||||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
class FollowingAccountsController < ApplicationController
|
class FollowingAccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
include WebAppControllerConcern
|
|
||||||
|
|
||||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,7 @@
|
||||||
class HomeController < ApplicationController
|
class HomeController < ApplicationController
|
||||||
include WebAppControllerConcern
|
include WebAppControllerConcern
|
||||||
|
|
||||||
before_action :set_instance_presenter
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,15 +5,7 @@ class PrivacyController < ApplicationController
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :set_instance_presenter
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,6 @@ class StatusesController < ApplicationController
|
||||||
|
|
||||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :set_instance_presenter
|
|
||||||
before_action :redirect_to_original, only: :show
|
before_action :redirect_to_original, only: :show
|
||||||
before_action :set_body_classes, only: :embed
|
before_action :set_body_classes, only: :embed
|
||||||
|
|
||||||
|
@ -68,10 +67,6 @@ class StatusesController < ApplicationController
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def redirect_to_original
|
def redirect_to_original
|
||||||
redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog?
|
redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog?
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,6 @@ class TagsController < ApplicationController
|
||||||
before_action :set_local
|
before_action :set_local
|
||||||
before_action :set_tag
|
before_action :set_tag
|
||||||
before_action :set_statuses, if: -> { request.format == :rss }
|
before_action :set_statuses, if: -> { request.format == :rss }
|
||||||
before_action :set_instance_presenter
|
|
||||||
|
|
||||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||||
|
|
||||||
|
@ -49,10 +48,6 @@ class TagsController < ApplicationController
|
||||||
@statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status)
|
@statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_instance_presenter
|
|
||||||
@instance_presenter = InstancePresenter.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def limit_param
|
def limit_param
|
||||||
params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||||
end
|
end
|
||||||
|
|
11
app/helpers/admin/announcements_helper.rb
Normal file
11
app/helpers/admin/announcements_helper.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::AnnouncementsHelper
|
||||||
|
def datetime_pattern
|
||||||
|
'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}'
|
||||||
|
end
|
||||||
|
|
||||||
|
def datetime_placeholder
|
||||||
|
Time.zone.now.strftime('%FT%R')
|
||||||
|
end
|
||||||
|
end
|
19
app/helpers/admin/disputes_helper.rb
Normal file
19
app/helpers/admin/disputes_helper.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
module DisputesHelper
|
||||||
|
def strike_action_label(appeal)
|
||||||
|
t(key_for_action(appeal),
|
||||||
|
scope: 'admin.strikes.actions',
|
||||||
|
name: content_tag(:span, appeal.strike.account.username, class: 'username'),
|
||||||
|
target: content_tag(:span, appeal.account.username, class: 'target'))
|
||||||
|
.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def key_for_action(appeal)
|
||||||
|
AccountWarning.actions.slice(appeal.strike.action).keys.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,11 +1,24 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module DatabaseHelper
|
module DatabaseHelper
|
||||||
|
def replica_enabled?
|
||||||
|
ENV['REPLICA_DB_NAME'] || ENV.fetch('REPLICA_DATABASE_URL', nil)
|
||||||
|
end
|
||||||
|
module_function :replica_enabled?
|
||||||
|
|
||||||
def with_read_replica(&block)
|
def with_read_replica(&block)
|
||||||
ApplicationRecord.connected_to(role: :reading, prevent_writes: true, &block)
|
if replica_enabled?
|
||||||
|
ApplicationRecord.connected_to(role: :reading, prevent_writes: true, &block)
|
||||||
|
else
|
||||||
|
yield
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_primary(&block)
|
def with_primary(&block)
|
||||||
ApplicationRecord.connected_to(role: :writing, &block)
|
if replica_enabled?
|
||||||
|
ApplicationRecord.connected_to(role: :writing, &block)
|
||||||
|
else
|
||||||
|
yield
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
11
app/helpers/invites_helper.rb
Normal file
11
app/helpers/invites_helper.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module InvitesHelper
|
||||||
|
def invites_max_uses_options
|
||||||
|
[1, 5, 10, 25, 50, 100]
|
||||||
|
end
|
||||||
|
|
||||||
|
def invites_expires_options
|
||||||
|
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week]
|
||||||
|
end
|
||||||
|
end
|
|
@ -230,6 +230,24 @@ module LanguagesHelper
|
||||||
'sr-Latn': 'Srpski (latinica)',
|
'sr-Latn': 'Srpski (latinica)',
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
# Helper for self.sorted_locale_keys
|
||||||
|
private_class_method def self.locale_name_for_sorting(locale)
|
||||||
|
if locale.blank? || locale == 'und'
|
||||||
|
'000'
|
||||||
|
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
|
||||||
|
ASCIIFolding.new.fold(supported_locale[1]).downcase
|
||||||
|
elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
|
||||||
|
ASCIIFolding.new.fold(regional_locale).downcase
|
||||||
|
else
|
||||||
|
locale
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sort locales by native name for dropdown menus
|
||||||
|
def self.sorted_locale_keys(locale_keys)
|
||||||
|
locale_keys.sort_by { |key, _| locale_name_for_sorting(key) }
|
||||||
|
end
|
||||||
|
|
||||||
def native_locale_name(locale)
|
def native_locale_name(locale)
|
||||||
if locale.blank? || locale == 'und'
|
if locale.blank? || locale == 'und'
|
||||||
I18n.t('generic.none')
|
I18n.t('generic.none')
|
||||||
|
@ -254,6 +272,7 @@ module LanguagesHelper
|
||||||
|
|
||||||
def valid_locale_or_nil(str)
|
def valid_locale_or_nil(str)
|
||||||
return if str.blank?
|
return if str.blank?
|
||||||
|
return str if valid_locale?(str)
|
||||||
|
|
||||||
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
|
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,6 @@ module MascotHelper
|
||||||
full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'))
|
full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def instance_presenter
|
def instance_presenter
|
||||||
@instance_presenter ||= InstancePresenter.new
|
@instance_presenter ||= InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
module RoutingHelper
|
module RoutingHelper
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include Rails.application.routes.url_helpers
|
|
||||||
include ActionView::Helpers::AssetTagHelper
|
include ActionView::Helpers::AssetTagHelper
|
||||||
include Webpacker::Helper
|
include Webpacker::Helper
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
def default_url_options
|
def default_url_options
|
||||||
ActionMailer::Base.default_url_options
|
ActionMailer::Base.default_url_options
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
module SettingsHelper
|
module SettingsHelper
|
||||||
def filterable_languages
|
def filterable_languages
|
||||||
LanguagesHelper::SUPPORTED_LOCALES.keys
|
LanguagesHelper.sorted_locale_keys(LanguagesHelper::SUPPORTED_LOCALES.keys)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ui_languages
|
||||||
|
LanguagesHelper.sorted_locale_keys(I18n.available_locales)
|
||||||
end
|
end
|
||||||
|
|
||||||
def session_device_icon(session)
|
def session_device_icon(session)
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
|
||||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
|
||||||
|
|
||||||
export function openDropdownMenu(id, keyboard, scroll_key) {
|
|
||||||
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function closeDropdownMenu(id) {
|
|
||||||
return { type: DROPDOWN_MENU_CLOSE, id };
|
|
||||||
}
|
|
11
app/javascript/flavours/glitch/actions/dropdown_menu.ts
Normal file
11
app/javascript/flavours/glitch/actions/dropdown_menu.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const openDropdownMenu = createAction<{
|
||||||
|
id: string;
|
||||||
|
keyboard: boolean;
|
||||||
|
scrollKey: string;
|
||||||
|
}>('dropdownMenu/open');
|
||||||
|
|
||||||
|
export const closeDropdownMenu = createAction<{ id: string }>(
|
||||||
|
'dropdownMenu/close',
|
||||||
|
);
|
|
@ -127,7 +127,7 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
||||||
normalStatus.media_attachments.forEach(item => {
|
normalStatus.media_attachments.forEach(item => {
|
||||||
const oldItem = list.find(i => i.get('id') === item.id);
|
const oldItem = list.find(i => i.get('id') === item.id);
|
||||||
if (oldItem && oldItem.get('description') === item.description) {
|
if (oldItem && oldItem.get('description') === item.description) {
|
||||||
item.translation = oldItem.get('translation')
|
item.translation = oldItem.get('translation');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -160,13 +160,13 @@ export function normalizePoll(poll, normalOldPoll) {
|
||||||
...option,
|
...option,
|
||||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||||
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||||
}
|
};
|
||||||
|
|
||||||
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
|
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
|
||||||
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
|
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalOption
|
return normalOption;
|
||||||
});
|
});
|
||||||
|
|
||||||
return normalPoll;
|
return normalPoll;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type { ModalProps } from 'flavours/glitch/reducers/modal';
|
||||||
|
|
||||||
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
|
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
|
||||||
|
|
||||||
export type ModalType = keyof typeof MODAL_COMPONENTS;
|
export type ModalType = keyof typeof MODAL_COMPONENTS;
|
||||||
|
|
||||||
interface OpenModalPayload {
|
interface OpenModalPayload {
|
||||||
modalType: ModalType;
|
modalType: ModalType;
|
||||||
modalProps: unknown;
|
modalProps: ModalProps;
|
||||||
}
|
}
|
||||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,12 @@ import api from 'flavours/glitch/api';
|
||||||
import { roundTo10 } from 'flavours/glitch/utils/numbers';
|
import { roundTo10 } from 'flavours/glitch/utils/numbers';
|
||||||
|
|
||||||
const dateForCohort = cohort => {
|
const dateForCohort = cohort => {
|
||||||
|
const timeZone = 'UTC';
|
||||||
switch(cohort.frequency) {
|
switch(cohort.frequency) {
|
||||||
case 'day':
|
case 'day':
|
||||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
|
||||||
default:
|
default:
|
||||||
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default class Column extends PureComponent {
|
||||||
if (scrollable.classList.contains('scrollable--flex')) {
|
if (scrollable.classList.contains('scrollable--flex')) {
|
||||||
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
|
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scrollable) {
|
if (!scrollable) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -4,9 +4,14 @@ import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dro
|
||||||
import { fetchHistory } from 'flavours/glitch/actions/history';
|
import { fetchHistory } from 'flavours/glitch/actions/history';
|
||||||
import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('flavours/glitch/store').RootState} state
|
||||||
|
* @param {*} props
|
||||||
|
*/
|
||||||
const mapStateToProps = (state, { statusId }) => ({
|
const mapStateToProps = (state, { statusId }) => ({
|
||||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
openDropdownId: state.dropdownMenu.openId,
|
||||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||||
items: state.getIn(['history', statusId, 'items']),
|
items: state.getIn(['history', statusId, 'items']),
|
||||||
loading: state.getIn(['history', statusId, 'loading']),
|
loading: state.getIn(['history', statusId, 'loading']),
|
||||||
});
|
});
|
||||||
|
@ -15,11 +20,11 @@ const mapDispatchToProps = (dispatch, { statusId }) => ({
|
||||||
|
|
||||||
onOpen (id, onItemClick, keyboard) {
|
onOpen (id, onItemClick, keyboard) {
|
||||||
dispatch(fetchHistory(statusId));
|
dispatch(fetchHistory(statusId));
|
||||||
dispatch(openDropdownMenu(id, keyboard));
|
dispatch(openDropdownMenu({ id, keyboard }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose (id) {
|
onClose (id) {
|
||||||
dispatch(closeDropdownMenu(id));
|
dispatch(closeDropdownMenu({ id }));
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -133,7 +133,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleReveal = () => {
|
handleReveal = () => {
|
||||||
this.setState({ revealed: true });
|
this.setState({ revealed: true });
|
||||||
}
|
};
|
||||||
|
|
||||||
renderOption (option, optionIndex, showResults) {
|
renderOption (option, optionIndex, showResults) {
|
||||||
const { poll, lang, disabled, intl } = this.props;
|
const { poll, lang, disabled, intl } = this.props;
|
||||||
|
|
|
@ -23,9 +23,14 @@ const MOUSE_IDLE_DELAY = 300;
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('flavours/glitch/store').RootState} state
|
||||||
|
* @param {*} props
|
||||||
|
*/
|
||||||
const mapStateToProps = (state, { scrollKey }) => {
|
const mapStateToProps = (state, { scrollKey }) => {
|
||||||
return {
|
return {
|
||||||
preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
|
preventScroll: scrollKey === state.dropdownMenu.scrollKey,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -73,7 +78,7 @@ class ScrollableList extends PureComponent {
|
||||||
const clientHeight = this.getClientHeight();
|
const clientHeight = this.getClientHeight();
|
||||||
const offset = scrollHeight - scrollTop - clientHeight;
|
const offset = scrollHeight - scrollTop - clientHeight;
|
||||||
|
|
||||||
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||||
this.props.onLoadMore();
|
this.props.onLoadMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,12 @@ import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
||||||
|
|
||||||
import { isUserTouching } from '../is_mobile';
|
import { isUserTouching } from '../is_mobile';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('flavours/glitch/store').RootState} state
|
||||||
|
*/
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
openDropdownId: state.dropdownMenu.openId,
|
||||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||||
|
@ -20,7 +23,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||||
actions: items,
|
actions: items,
|
||||||
onClick: onItemClick,
|
onClick: onItemClick,
|
||||||
},
|
},
|
||||||
}) : openDropdownMenu(id, keyboard, scrollKey));
|
}) : openDropdownMenu({ id, keyboard, scrollKey }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose(id) {
|
onClose(id) {
|
||||||
|
@ -28,7 +31,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||||
modalType: 'ACTIONS',
|
modalType: 'ACTIONS',
|
||||||
ignoreFocus: false,
|
ignoreFocus: false,
|
||||||
}));
|
}));
|
||||||
dispatch(closeDropdownMenu(id));
|
dispatch(closeDropdownMenu({ id }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -59,14 +59,14 @@ class Search extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultOptions = [
|
defaultOptions = [
|
||||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
|
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
|
||||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
|
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
|
||||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
|
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
|
||||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
|
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
|
||||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
|
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
|
||||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
|
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
|
||||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
|
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
|
||||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } }
|
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
|
||||||
];
|
];
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
|
@ -92,7 +92,7 @@ class Search extends PureComponent {
|
||||||
|
|
||||||
if (value.length > 0 || submitted) {
|
if (value.length > 0 || submitted) {
|
||||||
onClear();
|
onClear();
|
||||||
this.setState({ options: [], selectedOption: -1 })
|
this.setState({ options: [], selectedOption: -1 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
height: null,
|
height: null,
|
||||||
lineHeight: '27px',
|
lineHeight: '27px',
|
||||||
width: `${18 * 1.28571429}px`,
|
minWidth: `${18 * 1.28571429}px`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class TextIconButton extends PureComponent {
|
export default class TextIconButton extends PureComponent {
|
||||||
|
|
|
@ -80,7 +80,7 @@ class Results extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
handleSelectAll = () => {
|
handleSelectAll = () => {
|
||||||
const { submittedType, dispatch } = this.props;
|
const { submittedType, dispatch } = this.props;
|
||||||
|
@ -116,7 +116,7 @@ class Results extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ type: 'hashtags' });
|
this.setState({ type: 'hashtags' });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleSelectStatuses = () => {
|
handleSelectStatuses = () => {
|
||||||
const { submittedType, dispatch } = this.props;
|
const { submittedType, dispatch } = this.props;
|
||||||
|
@ -128,7 +128,7 @@ class Results extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ type: 'statuses' });
|
this.setState({ type: 'statuses' });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleLoadMoreAccounts = () => this._loadMore('accounts');
|
handleLoadMoreAccounts = () => this._loadMore('accounts');
|
||||||
handleLoadMoreStatuses = () => this._loadMore('statuses');
|
handleLoadMoreStatuses = () => this._loadMore('statuses');
|
||||||
|
@ -156,45 +156,43 @@ class Results extends PureComponent {
|
||||||
|
|
||||||
let filteredResults;
|
let filteredResults;
|
||||||
|
|
||||||
if (!isLoading) {
|
const accounts = results.get('accounts', ImmutableList());
|
||||||
const accounts = results.get('accounts', ImmutableList());
|
const hashtags = results.get('hashtags', ImmutableList());
|
||||||
const hashtags = results.get('hashtags', ImmutableList());
|
const statuses = results.get('statuses', ImmutableList());
|
||||||
const statuses = results.get('statuses', ImmutableList());
|
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'all':
|
case 'all':
|
||||||
filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? (
|
filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? (
|
||||||
<>
|
<>
|
||||||
{accounts.size > 0 && (
|
{accounts.size > 0 && (
|
||||||
<SearchSection key='accounts' title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}>
|
<SearchSection key='accounts' title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}>
|
||||||
{accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)}
|
{accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)}
|
||||||
</SearchSection>
|
</SearchSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hashtags.size > 0 && (
|
{hashtags.size > 0 && (
|
||||||
<SearchSection key='hashtags' title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}>
|
<SearchSection key='hashtags' title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}>
|
||||||
{hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
{hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||||
</SearchSection>
|
</SearchSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{statuses.size > 0 && (
|
{statuses.size > 0 && (
|
||||||
<SearchSection key='statuses' title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}>
|
<SearchSection key='statuses' title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}>
|
||||||
{statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} />)}
|
{statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} />)}
|
||||||
</SearchSection>
|
</SearchSection>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : [];
|
) : [];
|
||||||
break;
|
break;
|
||||||
case 'accounts':
|
case 'accounts':
|
||||||
filteredResults = renderAccounts(accounts);
|
filteredResults = renderAccounts(accounts);
|
||||||
break;
|
break;
|
||||||
case 'hashtags':
|
case 'hashtags':
|
||||||
filteredResults = renderHashtags(hashtags);
|
filteredResults = renderHashtags(hashtags);
|
||||||
break;
|
break;
|
||||||
case 'statuses':
|
case 'statuses':
|
||||||
filteredResults = renderStatuses(statuses);
|
filteredResults = renderStatuses(statuses);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -221,7 +221,7 @@ const Firehose = ({ feedType, multiColumn }) => {
|
||||||
</Helmet>
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Firehose.propTypes = {
|
Firehose.propTypes = {
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
|
|
@ -37,10 +37,19 @@ const getHomeFeedSpeed = createSelector([
|
||||||
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
|
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
|
||||||
state => state.get('statuses'),
|
state => state.get('statuses'),
|
||||||
], (statusIds, pendingStatusIds, statusMap) => {
|
], (statusIds, pendingStatusIds, statusMap) => {
|
||||||
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
const recentStatusIds = pendingStatusIds.concat(statusIds);
|
||||||
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
|
||||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
if (statuses.isEmpty()) {
|
||||||
|
return {
|
||||||
|
gap: 0,
|
||||||
|
newest: new Date(0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const datetimes = statuses.map(status => status.get('created_at', 0));
|
||||||
|
const oldest = new Date(datetimes.min());
|
||||||
|
const newest = new Date(datetimes.max());
|
||||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -55,8 +64,10 @@ const homeTooSlow = createSelector([
|
||||||
getHomeFeedSpeed,
|
getHomeFeedSpeed,
|
||||||
], (isLoading, isPartial, speed) =>
|
], (isLoading, isPartial, speed) =>
|
||||||
!isLoading && !isPartial // Only if the home feed has finished loading
|
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||||
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
&& (
|
||||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
(speed.gap > (30 * 60) // If the average gap between posts is more than 30 minutes
|
||||||
|
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -27,9 +27,9 @@ const mapStateToProps = (state, { accountId }) => ({
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
onSignupClick() {
|
onSignupClick() {
|
||||||
dispatch(closeModal({
|
dispatch(closeModal({
|
||||||
modalType: undefined,
|
modalType: undefined,
|
||||||
ignoreFocus: false,
|
ignoreFocus: false,
|
||||||
}));
|
}));
|
||||||
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -187,7 +187,7 @@ class LoginForm extends React.PureComponent {
|
||||||
|
|
||||||
setIFrameRef = (iframe) => {
|
setIFrameRef = (iframe) => {
|
||||||
this.iframeRef = iframe;
|
this.iframeRef = iframe;
|
||||||
}
|
};
|
||||||
|
|
||||||
handleFocus = () => {
|
handleFocus = () => {
|
||||||
this.setState({ expanded: true });
|
this.setState({ expanded: true });
|
||||||
|
|
|
@ -104,7 +104,7 @@ const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedD
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Comment.propTypes = {
|
Comment.propTypes = {
|
||||||
comment: PropTypes.string.isRequired,
|
comment: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -218,6 +218,7 @@ class Status extends ImmutablePureComponent {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
attachFullscreenListener(this.onFullScreenChange);
|
attachFullscreenListener(this.onFullScreenChange);
|
||||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||||
|
this._scrollStatusIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
@ -630,10 +631,10 @@ class Status extends ImmutablePureComponent {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
_scrollStatusIntoView () {
|
||||||
const { status, ancestorsIds, multiColumn } = this.props;
|
const { status, multiColumn } = this.props;
|
||||||
|
|
||||||
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
|
if (status) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
|
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
|
||||||
|
|
||||||
|
@ -650,6 +651,14 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
const { status, ancestorsIds } = this.props;
|
||||||
|
|
||||||
|
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
|
||||||
|
this._scrollStatusIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
detachFullscreenListener(this.onFullScreenChange);
|
detachFullscreenListener(this.onFullScreenChange);
|
||||||
}
|
}
|
||||||
|
@ -658,6 +667,22 @@ class Status extends ImmutablePureComponent {
|
||||||
this.setState({ fullscreen: isFullscreen() });
|
this.setState({ fullscreen: isFullscreen() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||||
|
// Do not change scroll when opening a modal
|
||||||
|
if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to focused post if it is loaded
|
||||||
|
const child = this.node?.querySelector('.detailed-status__wrapper');
|
||||||
|
if (child) {
|
||||||
|
return [0, child.offsetTop];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not scroll otherwise, `componentDidUpdate` will take care of that
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let ancestors, descendants;
|
let ancestors, descendants;
|
||||||
const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
||||||
|
@ -717,7 +742,7 @@ class Status extends ImmutablePureComponent {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollContainer scrollKey='thread'>
|
<ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
|
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
|
||||||
{ancestors}
|
{ancestors}
|
||||||
|
|
||||||
|
|
|
@ -77,8 +77,8 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
if (sso_redirect) {
|
if (sso_redirect) {
|
||||||
content = (
|
content = (
|
||||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
let signupButton;
|
let signupButton;
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ class LinkFooter extends PureComponent {
|
||||||
{DividingCircle}
|
{DividingCircle}
|
||||||
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
||||||
{DividingCircle}
|
{DividingCircle}
|
||||||
v{version}
|
<span className='version'>v{version}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -128,7 +128,7 @@ export default class ModalRoot extends PureComponent {
|
||||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||||
{(SpecificComponent) => {
|
{(SpecificComponent) => {
|
||||||
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
|
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
|
||||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />
|
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;
|
||||||
}}
|
}}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
|
|
||||||
|
|
|
@ -53,20 +53,20 @@ class NavigationPanel extends Component {
|
||||||
const { intl, onOpenSettings } = this.props;
|
const { intl, onOpenSettings } = this.props;
|
||||||
const { signedIn, disabledAccountId } = this.context.identity;
|
const { signedIn, disabledAccountId } = this.context.identity;
|
||||||
|
|
||||||
|
let banner = undefined;
|
||||||
|
|
||||||
|
if(transientSingleColumn)
|
||||||
|
banner = (<div className='switch-to-advanced'>
|
||||||
|
{intl.formatMessage(messages.openedInClassicInterface)}
|
||||||
|
{" "}
|
||||||
|
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
|
||||||
|
{intl.formatMessage(messages.advancedInterface)}
|
||||||
|
</a>
|
||||||
|
</div>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='navigation-panel'>
|
<div className='navigation-panel'>
|
||||||
{transientSingleColumn && (
|
{banner}
|
||||||
<div className='navigation-panel__logo'>
|
|
||||||
<div class='switch-to-advanced'>
|
|
||||||
{intl.formatMessage(messages.openedInClassicInterface)}
|
|
||||||
{" "}
|
|
||||||
<a href={`/deck${location.pathname}`} class='switch-to-advanced__toggle'>
|
|
||||||
{intl.formatMessage(messages.advancedInterface)}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const SignInBanner = () => {
|
||||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
|
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
|
||||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registrationsOpen) {
|
if (registrationsOpen) {
|
||||||
|
|
|
@ -80,7 +80,7 @@ const mapStateToProps = state => ({
|
||||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||||
isWide: state.getIn(['local_settings', 'stretch']),
|
isWide: state.getIn(['local_settings', 'stretch']),
|
||||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
dropdownMenuIsOpen: state.dropdownMenu.openId !== null,
|
||||||
unreadNotifications: state.getIn(['notifications', 'unread']),
|
unreadNotifications: state.getIn(['notifications', 'unread']),
|
||||||
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
|
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
|
||||||
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
|
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
|
||||||
|
@ -191,7 +191,9 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
|
|
||||||
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
||||||
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
|
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
|
||||||
|
{/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */}
|
||||||
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
|
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
|
||||||
|
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}
|
||||||
|
|
||||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||||
|
|
|
@ -94,6 +94,7 @@ const initialPath = document.querySelector("head meta[name=initialPath]")?.getAt
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
export const hasMultiColumnPath = initialPath === '/'
|
export const hasMultiColumnPath = initialPath === '/'
|
||||||
|| initialPath === '/getting-started'
|
|| initialPath === '/getting-started'
|
||||||
|
|| initialPath === '/home'
|
||||||
|| initialPath.startsWith('/deck');
|
|| initialPath.startsWith('/deck');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import Immutable from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DROPDOWN_MENU_OPEN,
|
|
||||||
DROPDOWN_MENU_CLOSE,
|
|
||||||
} from '../actions/dropdown_menu';
|
|
||||||
|
|
||||||
const initialState = Immutable.Map({ openId: null, keyboard: false, scroll_key: null });
|
|
||||||
|
|
||||||
export default function dropdownMenu(state = initialState, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case DROPDOWN_MENU_OPEN:
|
|
||||||
return state.merge({ openId: action.id, keyboard: action.keyboard, scroll_key: action.scroll_key });
|
|
||||||
case DROPDOWN_MENU_CLOSE:
|
|
||||||
return state.get('openId') === action.id ? state.set('openId', null).set('scroll_key', null) : state;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
33
app/javascript/flavours/glitch/reducers/dropdown_menu.ts
Normal file
33
app/javascript/flavours/glitch/reducers/dropdown_menu.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { closeDropdownMenu, openDropdownMenu } from '../actions/dropdown_menu';
|
||||||
|
|
||||||
|
interface DropdownMenuState {
|
||||||
|
openId: string | null;
|
||||||
|
keyboard: boolean;
|
||||||
|
scrollKey: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: DropdownMenuState = {
|
||||||
|
openId: null,
|
||||||
|
keyboard: false,
|
||||||
|
scrollKey: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dropdownMenuReducer = createReducer(initialState, (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(
|
||||||
|
openDropdownMenu,
|
||||||
|
(state, { payload: { id, keyboard, scrollKey } }) => {
|
||||||
|
state.openId = id;
|
||||||
|
state.keyboard = keyboard;
|
||||||
|
state.scrollKey = scrollKey;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.addCase(closeDropdownMenu, (state, { payload: { id } }) => {
|
||||||
|
if (state.openId === id) {
|
||||||
|
state.openId = null;
|
||||||
|
state.scrollKey = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -16,7 +16,7 @@ import contexts from './contexts';
|
||||||
import conversations from './conversations';
|
import conversations from './conversations';
|
||||||
import custom_emojis from './custom_emojis';
|
import custom_emojis from './custom_emojis';
|
||||||
import domain_lists from './domain_lists';
|
import domain_lists from './domain_lists';
|
||||||
import dropdown_menu from './dropdown_menu';
|
import { dropdownMenuReducer } from './dropdown_menu';
|
||||||
import filters from './filters';
|
import filters from './filters';
|
||||||
import followed_tags from './followed_tags';
|
import followed_tags from './followed_tags';
|
||||||
import height_cache from './height_cache';
|
import height_cache from './height_cache';
|
||||||
|
@ -49,7 +49,7 @@ import user_lists from './user_lists';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
announcements,
|
announcements,
|
||||||
dropdown_menu,
|
dropdownMenu: dropdownMenuReducer,
|
||||||
timelines,
|
timelines,
|
||||||
meta,
|
meta,
|
||||||
alerts,
|
alerts,
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Record as ImmutableRecord, Stack } from 'immutable';
|
import { Record as ImmutableRecord, Stack } from 'immutable';
|
||||||
|
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { Reducer } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
|
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
|
||||||
import type { ModalType } from '../actions/modal';
|
import type { ModalType } from '../actions/modal';
|
||||||
import { openModal, closeModal } from '../actions/modal';
|
import { openModal, closeModal } from '../actions/modal';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
|
|
||||||
type ModalProps = Record<string, unknown>;
|
export type ModalProps = Record<string, unknown>;
|
||||||
interface Modal {
|
interface Modal {
|
||||||
modalType: ModalType;
|
modalType: ModalType;
|
||||||
modalProps: ModalProps;
|
modalProps: ModalProps;
|
||||||
|
@ -62,33 +62,22 @@ const pushModal = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function modalReducer(
|
export const modalReducer: Reducer<State> = (state = initialState, action) => {
|
||||||
state: State = initialState,
|
if (openModal.match(action))
|
||||||
action: PayloadAction<{
|
return pushModal(
|
||||||
modalType: ModalType;
|
state,
|
||||||
ignoreFocus: boolean;
|
action.payload.modalType,
|
||||||
modalProps: Record<string, unknown>;
|
action.payload.modalProps,
|
||||||
}>,
|
);
|
||||||
) {
|
else if (closeModal.match(action)) return popModal(state, action.payload);
|
||||||
switch (action.type) {
|
// TODO: type those actions
|
||||||
case openModal.type:
|
else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS)
|
||||||
return pushModal(
|
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
|
||||||
state,
|
else if (action.type === TIMELINE_DELETE)
|
||||||
action.payload.modalType,
|
return state.update('stack', (stack) =>
|
||||||
action.payload.modalProps,
|
stack.filterNot(
|
||||||
);
|
(modal) => modal.get('modalProps').statusId === action.id,
|
||||||
case closeModal.type:
|
),
|
||||||
return popModal(state, action.payload);
|
);
|
||||||
case COMPOSE_UPLOAD_CHANGE_SUCCESS:
|
else return state;
|
||||||
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
|
};
|
||||||
case TIMELINE_DELETE:
|
|
||||||
return state.update('stack', (stack) =>
|
|
||||||
stack.filterNot(
|
|
||||||
// @ts-expect-error TIMELINE_DELETE action is not typed yet.
|
|
||||||
(modal) => modal.get('modalProps').statusId === action.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -41,6 +41,10 @@
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.version {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
|
@ -281,6 +281,7 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
|
white-space: nowrap;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
transition: all 100ms ease-in;
|
transition: all 100ms ease-in;
|
||||||
transition-property: background-color, color;
|
transition-property: background-color, color;
|
||||||
|
|
|
@ -273,6 +273,7 @@
|
||||||
|
|
||||||
.navigation-panel__sign-in-banner,
|
.navigation-panel__sign-in-banner,
|
||||||
.navigation-panel__logo,
|
.navigation-panel__logo,
|
||||||
|
.navigation-panel__banner,
|
||||||
.getting-started__trends {
|
.getting-started__trends {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,4 +56,4 @@ export const showAlertForError = (error, skipNotFound = false) => {
|
||||||
title: messages.unexpectedTitle,
|
title: messages.unexpectedTitle,
|
||||||
message: messages.unexpectedMessage,
|
message: messages.unexpectedMessage,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
|
||||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
|
||||||
|
|
||||||
export function openDropdownMenu(id, keyboard, scroll_key) {
|
|
||||||
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function closeDropdownMenu(id) {
|
|
||||||
return { type: DROPDOWN_MENU_CLOSE, id };
|
|
||||||
}
|
|
11
app/javascript/mastodon/actions/dropdown_menu.ts
Normal file
11
app/javascript/mastodon/actions/dropdown_menu.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const openDropdownMenu = createAction<{
|
||||||
|
id: string;
|
||||||
|
keyboard: boolean;
|
||||||
|
scrollKey: string;
|
||||||
|
}>('dropdownMenu/open');
|
||||||
|
|
||||||
|
export const closeDropdownMenu = createAction<{ id: string }>(
|
||||||
|
'dropdownMenu/close',
|
||||||
|
);
|
|
@ -104,7 +104,7 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
normalStatus.media_attachments.forEach(item => {
|
normalStatus.media_attachments.forEach(item => {
|
||||||
const oldItem = list.find(i => i.get('id') === item.id);
|
const oldItem = list.find(i => i.get('id') === item.id);
|
||||||
if (oldItem && oldItem.get('description') === item.description) {
|
if (oldItem && oldItem.get('description') === item.description) {
|
||||||
item.translation = oldItem.get('translation')
|
item.translation = oldItem.get('translation');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -137,13 +137,13 @@ export function normalizePoll(poll, normalOldPoll) {
|
||||||
...option,
|
...option,
|
||||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||||
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||||
}
|
};
|
||||||
|
|
||||||
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
|
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
|
||||||
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
|
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalOption
|
return normalOption;
|
||||||
});
|
});
|
||||||
|
|
||||||
return normalPoll;
|
return normalPoll;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type { ModalProps } from 'mastodon/reducers/modal';
|
||||||
|
|
||||||
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
|
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
|
||||||
|
|
||||||
export type ModalType = keyof typeof MODAL_COMPONENTS;
|
export type ModalType = keyof typeof MODAL_COMPONENTS;
|
||||||
|
|
||||||
interface OpenModalPayload {
|
interface OpenModalPayload {
|
||||||
modalType: ModalType;
|
modalType: ModalType;
|
||||||
modalProps: unknown;
|
modalProps: ModalProps;
|
||||||
}
|
}
|
||||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,10 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
||||||
const signedIn = !!getState().getIn(['meta', 'me']);
|
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||||
|
|
||||||
if (!signedIn) {
|
if (!signedIn) {
|
||||||
|
if (onFailure) {
|
||||||
|
onFailure();
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
45
app/javascript/mastodon/api_types/accounts.ts
Normal file
45
app/javascript/mastodon/api_types/accounts.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||||
|
|
||||||
|
export interface ApiAccountFieldJSON {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
verified_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiAccountRoleJSON {
|
||||||
|
color: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See app/serializers/rest/account_serializer.rb
|
||||||
|
export interface ApiAccountJSON {
|
||||||
|
acct: string;
|
||||||
|
avatar: string;
|
||||||
|
avatar_static: string;
|
||||||
|
bot: boolean;
|
||||||
|
created_at: string;
|
||||||
|
discoverable: boolean;
|
||||||
|
display_name: string;
|
||||||
|
emojis: ApiCustomEmojiJSON[];
|
||||||
|
fields: ApiAccountFieldJSON[];
|
||||||
|
followers_count: number;
|
||||||
|
following_count: number;
|
||||||
|
group: boolean;
|
||||||
|
header: string;
|
||||||
|
header_static: string;
|
||||||
|
id: string;
|
||||||
|
last_status_at: string;
|
||||||
|
locked: boolean;
|
||||||
|
noindex: boolean;
|
||||||
|
note: string;
|
||||||
|
roles: ApiAccountJSON[];
|
||||||
|
statuses_count: number;
|
||||||
|
uri: string;
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
moved?: ApiAccountJSON;
|
||||||
|
suspended?: boolean;
|
||||||
|
limited?: boolean;
|
||||||
|
memorial?: boolean;
|
||||||
|
}
|
8
app/javascript/mastodon/api_types/custom_emoji.ts
Normal file
8
app/javascript/mastodon/api_types/custom_emoji.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// See app/serializers/rest/account_serializer.rb
|
||||||
|
export interface ApiCustomEmojiJSON {
|
||||||
|
shortcode: string;
|
||||||
|
static_url: string;
|
||||||
|
url: string;
|
||||||
|
category?: string;
|
||||||
|
visible_in_picker: boolean;
|
||||||
|
}
|
18
app/javascript/mastodon/api_types/relationships.ts
Normal file
18
app/javascript/mastodon/api_types/relationships.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// See app/serializers/rest/relationship_serializer.rb
|
||||||
|
export interface ApiRelationshipJSON {
|
||||||
|
blocked_by: boolean;
|
||||||
|
blocking: boolean;
|
||||||
|
domain_blocking: boolean;
|
||||||
|
endorsed: boolean;
|
||||||
|
followed_by: boolean;
|
||||||
|
following: boolean;
|
||||||
|
id: string;
|
||||||
|
languages: string[] | null;
|
||||||
|
muting_notifications: boolean;
|
||||||
|
muting: boolean;
|
||||||
|
note: string;
|
||||||
|
notifying: boolean;
|
||||||
|
requested_by: boolean;
|
||||||
|
requested: boolean;
|
||||||
|
showing_reblogs: boolean;
|
||||||
|
}
|
|
@ -45,6 +45,21 @@ describe('computeHashtagBarForStatus', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not truncate the contents when the last child is a text node', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
|
||||||
|
['test'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('extract tags from the last line', () => {
|
it('extract tags from the last line', () => {
|
||||||
const status = createStatus(
|
const status = createStatus(
|
||||||
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
|
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
|
||||||
|
|
|
@ -9,11 +9,12 @@ import api from 'mastodon/api';
|
||||||
import { roundTo10 } from 'mastodon/utils/numbers';
|
import { roundTo10 } from 'mastodon/utils/numbers';
|
||||||
|
|
||||||
const dateForCohort = cohort => {
|
const dateForCohort = cohort => {
|
||||||
|
const timeZone = 'UTC';
|
||||||
switch(cohort.frequency) {
|
switch(cohort.frequency) {
|
||||||
case 'day':
|
case 'day':
|
||||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
|
||||||
default:
|
default:
|
||||||
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,13 +22,7 @@ export default class Column extends PureComponent {
|
||||||
scrollable = document.scrollingElement;
|
scrollable = document.scrollingElement;
|
||||||
} else {
|
} else {
|
||||||
scrollable = this.node.querySelector('.scrollable');
|
scrollable = this.node.querySelector('.scrollable');
|
||||||
|
}
|
||||||
// Some columns have nested `.scrollable` containers, with the outer one
|
|
||||||
// being a wrapper while the actual scrollable content is deeper.
|
|
||||||
if (scrollable.classList.contains('scrollable--flex')) {
|
|
||||||
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scrollable) {
|
if (!scrollable) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -4,9 +4,14 @@ import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_m
|
||||||
import { fetchHistory } from 'mastodon/actions/history';
|
import { fetchHistory } from 'mastodon/actions/history';
|
||||||
import DropdownMenu from 'mastodon/components/dropdown_menu';
|
import DropdownMenu from 'mastodon/components/dropdown_menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('mastodon/store').RootState} state
|
||||||
|
* @param {*} props
|
||||||
|
*/
|
||||||
const mapStateToProps = (state, { statusId }) => ({
|
const mapStateToProps = (state, { statusId }) => ({
|
||||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
openDropdownId: state.dropdownMenu.openId,
|
||||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||||
items: state.getIn(['history', statusId, 'items']),
|
items: state.getIn(['history', statusId, 'items']),
|
||||||
loading: state.getIn(['history', statusId, 'loading']),
|
loading: state.getIn(['history', statusId, 'loading']),
|
||||||
});
|
});
|
||||||
|
@ -15,11 +20,11 @@ const mapDispatchToProps = (dispatch, { statusId }) => ({
|
||||||
|
|
||||||
onOpen (id, onItemClick, keyboard) {
|
onOpen (id, onItemClick, keyboard) {
|
||||||
dispatch(fetchHistory(statusId));
|
dispatch(fetchHistory(statusId));
|
||||||
dispatch(openDropdownMenu(id, keyboard));
|
dispatch(openDropdownMenu({ id, keyboard }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose (id) {
|
onClose (id) {
|
||||||
dispatch(closeDropdownMenu(id));
|
dispatch(closeDropdownMenu({ id }));
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -109,7 +109,7 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
||||||
|
|
||||||
const lastChild = template.content.lastChild;
|
const lastChild = template.content.lastChild;
|
||||||
|
|
||||||
if (!lastChild) return defaultResult;
|
if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
|
||||||
|
|
||||||
template.content.removeChild(lastChild);
|
template.content.removeChild(lastChild);
|
||||||
const contentWithoutLastLine = template;
|
const contentWithoutLastLine = template;
|
||||||
|
|
|
@ -132,7 +132,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleReveal = () => {
|
handleReveal = () => {
|
||||||
this.setState({ revealed: true });
|
this.setState({ revealed: true });
|
||||||
}
|
};
|
||||||
|
|
||||||
renderOption (option, optionIndex, showResults) {
|
renderOption (option, optionIndex, showResults) {
|
||||||
const { poll, lang, disabled, intl } = this.props;
|
const { poll, lang, disabled, intl } = this.props;
|
||||||
|
|
|
@ -23,9 +23,14 @@ const MOUSE_IDLE_DELAY = 300;
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('mastodon/store').RootState} state
|
||||||
|
* @param {*} props
|
||||||
|
*/
|
||||||
const mapStateToProps = (state, { scrollKey }) => {
|
const mapStateToProps = (state, { scrollKey }) => {
|
||||||
return {
|
return {
|
||||||
preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
|
preventScroll: scrollKey === state.dropdownMenu.scrollKey,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -73,7 +78,7 @@ class ScrollableList extends PureComponent {
|
||||||
const clientHeight = this.getClientHeight();
|
const clientHeight = this.getClientHeight();
|
||||||
const offset = scrollHeight - scrollTop - clientHeight;
|
const offset = scrollHeight - scrollTop - clientHeight;
|
||||||
|
|
||||||
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||||
this.props.onLoadMore();
|
this.props.onLoadMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -199,7 +199,7 @@ class Status extends ImmutablePureComponent {
|
||||||
} else if (attachments.getIn([0, 'type']) === 'audio') {
|
} else if (attachments.getIn([0, 'type']) === 'audio') {
|
||||||
return '16 / 9';
|
return '16 / 9';
|
||||||
} else {
|
} else {
|
||||||
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2'
|
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,12 @@ import { openModal, closeModal } from '../actions/modal';
|
||||||
import DropdownMenu from '../components/dropdown_menu';
|
import DropdownMenu from '../components/dropdown_menu';
|
||||||
import { isUserTouching } from '../is_mobile';
|
import { isUserTouching } from '../is_mobile';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('mastodon/store').RootState} state
|
||||||
|
*/
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
openDropdownId: state.dropdownMenu.openId,
|
||||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||||
|
@ -25,7 +28,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||||
actions: items,
|
actions: items,
|
||||||
onClick: onItemClick,
|
onClick: onItemClick,
|
||||||
},
|
},
|
||||||
}) : openDropdownMenu(id, keyboard, scrollKey));
|
}) : openDropdownMenu({ id, keyboard, scrollKey }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose(id) {
|
onClose(id) {
|
||||||
|
@ -33,7 +36,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||||
modalType: 'ACTIONS',
|
modalType: 'ACTIONS',
|
||||||
ignoreFocus: false,
|
ignoreFocus: false,
|
||||||
}));
|
}));
|
||||||
dispatch(closeDropdownMenu(id));
|
dispatch(closeDropdownMenu({ id }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const username = this.props.account.get('acct')
|
const username = this.props.account.get('acct');
|
||||||
return (
|
return (
|
||||||
<div className='navigation-bar'>
|
<div className='navigation-bar'>
|
||||||
<Link to={`/@${username}`}>
|
<Link to={`/@${username}`}>
|
||||||
|
|
|
@ -57,14 +57,14 @@ class Search extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultOptions = [
|
defaultOptions = [
|
||||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
|
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
|
||||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
|
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
|
||||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
|
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
|
||||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
|
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
|
||||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
|
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
|
||||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
|
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
|
||||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
|
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
|
||||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } }
|
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
|
||||||
];
|
];
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
height: null,
|
height: null,
|
||||||
lineHeight: '27px',
|
lineHeight: '27px',
|
||||||
width: `${18 * 1.28571429}px`,
|
minWidth: `${18 * 1.28571429}px`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class TextIconButton extends PureComponent {
|
export default class TextIconButton extends PureComponent {
|
||||||
|
|
|
@ -67,47 +67,45 @@ class Explore extends PureComponent {
|
||||||
<Search />
|
<Search />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='scrollable scrollable--flex' data-nosnippet>
|
{isSearching ? (
|
||||||
{isSearching ? (
|
<SearchResults />
|
||||||
<SearchResults />
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
<div className='account__section-headline'>
|
||||||
<div className='account__section-headline'>
|
<NavLink exact to='/explore'>
|
||||||
<NavLink exact to='/explore'>
|
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
|
||||||
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink exact to='/explore/tags'>
|
||||||
|
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
{signedIn && (
|
||||||
|
<NavLink exact to='/explore/suggestions'>
|
||||||
|
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
)}
|
||||||
|
|
||||||
<NavLink exact to='/explore/tags'>
|
<NavLink exact to='/explore/links'>
|
||||||
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
|
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
{signedIn && (
|
<Switch>
|
||||||
<NavLink exact to='/explore/suggestions'>
|
<Route path='/explore/tags' component={Tags} />
|
||||||
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
|
<Route path='/explore/links' component={Links} />
|
||||||
</NavLink>
|
<Route path='/explore/suggestions' component={Suggestions} />
|
||||||
)}
|
<Route exact path={['/explore', '/explore/posts', '/search']}>
|
||||||
|
<Statuses multiColumn={multiColumn} />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
|
||||||
<NavLink exact to='/explore/links'>
|
<Helmet>
|
||||||
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
</NavLink>
|
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
||||||
</div>
|
</Helmet>
|
||||||
|
</>
|
||||||
<Switch>
|
)}
|
||||||
<Route path='/explore/tags' component={Tags} />
|
|
||||||
<Route path='/explore/links' component={Links} />
|
|
||||||
<Route path='/explore/suggestions' component={Suggestions} />
|
|
||||||
<Route exact path={['/explore', '/explore/posts', '/search']}>
|
|
||||||
<Statuses multiColumn={multiColumn} />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
|
|
||||||
<Helmet>
|
|
||||||
<title>{intl.formatMessage(messages.title)}</title>
|
|
||||||
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
|
||||||
</Helmet>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ class Links extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='explore__links'>
|
<div className='explore__links scrollable' data-nosnippet>
|
||||||
{banner}
|
{banner}
|
||||||
|
|
||||||
{isLoading ? (<LoadingIndicator />) : links.map((link, i) => (
|
{isLoading ? (<LoadingIndicator />) : links.map((link, i) => (
|
||||||
|
|
|
@ -80,7 +80,7 @@ class Results extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
handleSelectAll = () => {
|
handleSelectAll = () => {
|
||||||
const { submittedType, dispatch } = this.props;
|
const { submittedType, dispatch } = this.props;
|
||||||
|
@ -116,7 +116,7 @@ class Results extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ type: 'hashtags' });
|
this.setState({ type: 'hashtags' });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleSelectStatuses = () => {
|
handleSelectStatuses = () => {
|
||||||
const { submittedType, dispatch } = this.props;
|
const { submittedType, dispatch } = this.props;
|
||||||
|
@ -128,7 +128,7 @@ class Results extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ type: 'statuses' });
|
this.setState({ type: 'statuses' });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleLoadMoreAccounts = () => this._loadMore('accounts');
|
handleLoadMoreAccounts = () => this._loadMore('accounts');
|
||||||
handleLoadMoreStatuses = () => this._loadMore('statuses');
|
handleLoadMoreStatuses = () => this._loadMore('statuses');
|
||||||
|
@ -156,45 +156,43 @@ class Results extends PureComponent {
|
||||||
|
|
||||||
let filteredResults;
|
let filteredResults;
|
||||||
|
|
||||||
if (!isLoading) {
|
const accounts = results.get('accounts', ImmutableList());
|
||||||
const accounts = results.get('accounts', ImmutableList());
|
const hashtags = results.get('hashtags', ImmutableList());
|
||||||
const hashtags = results.get('hashtags', ImmutableList());
|
const statuses = results.get('statuses', ImmutableList());
|
||||||
const statuses = results.get('statuses', ImmutableList());
|
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'all':
|
case 'all':
|
||||||
filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? (
|
filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? (
|
||||||
<>
|
<>
|
||||||
{accounts.size > 0 && (
|
{accounts.size > 0 && (
|
||||||
<SearchSection key='accounts' title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}>
|
<SearchSection key='accounts' title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}>
|
||||||
{accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)}
|
{accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)}
|
||||||
</SearchSection>
|
</SearchSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hashtags.size > 0 && (
|
{hashtags.size > 0 && (
|
||||||
<SearchSection key='hashtags' title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}>
|
<SearchSection key='hashtags' title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}>
|
||||||
{hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
{hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||||
</SearchSection>
|
</SearchSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{statuses.size > 0 && (
|
{statuses.size > 0 && (
|
||||||
<SearchSection key='statuses' title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}>
|
<SearchSection key='statuses' title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}>
|
||||||
{statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} />)}
|
{statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} />)}
|
||||||
</SearchSection>
|
</SearchSection>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : [];
|
) : [];
|
||||||
break;
|
break;
|
||||||
case 'accounts':
|
case 'accounts':
|
||||||
filteredResults = renderAccounts(accounts);
|
filteredResults = renderAccounts(accounts);
|
||||||
break;
|
break;
|
||||||
case 'hashtags':
|
case 'hashtags':
|
||||||
filteredResults = renderHashtags(hashtags);
|
filteredResults = renderHashtags(hashtags);
|
||||||
break;
|
break;
|
||||||
case 'statuses':
|
case 'statuses':
|
||||||
filteredResults = renderStatuses(statuses);
|
filteredResults = renderStatuses(statuses);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -206,7 +204,7 @@ class Results extends PureComponent {
|
||||||
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
|
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='explore__search-results'>
|
<div className='explore__search-results' data-nosnippet>
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='search-results'
|
scrollKey='search-results'
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Suggestions extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='explore__suggestions'>
|
<div className='explore__suggestions scrollable' data-nosnippet>
|
||||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
||||||
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
|
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -51,7 +51,7 @@ class Tags extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='explore__links'>
|
<div className='scrollable explore__links' data-nosnippet>
|
||||||
{banner}
|
{banner}
|
||||||
|
|
||||||
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue