Compare commits
2 Commits
main
...
use-large-
Author | SHA1 | Date |
---|---|---|
Renaud Chaput | 1d01a480ae | |
Renaud Chaput | 2d79bbe5ff |
|
@ -325,8 +325,8 @@ module.exports = {
|
||||||
|
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/strict-type-checked',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:@typescript-eslint/stylistic-type-checked',
|
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
'plugin:jsx-a11y/recommended',
|
'plugin:jsx-a11y/recommended',
|
||||||
|
@ -338,7 +338,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: true,
|
project: './tsconfig.json',
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -348,7 +348,6 @@ module.exports = {
|
||||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
||||||
'@typescript-eslint/consistent-type-exports': 'error',
|
'@typescript-eslint/consistent-type-exports': 'error',
|
||||||
'@typescript-eslint/consistent-type-imports': 'error',
|
'@typescript-eslint/consistent-type-imports': 'error',
|
||||||
"@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}],
|
|
||||||
|
|
||||||
'jsdoc/require-jsdoc': 'off',
|
'jsdoc/require-jsdoc': 'off',
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
// Ignore major version bumps for these node packages
|
// Ignore major version bumps for these node packages
|
||||||
matchManagers: ['npm'],
|
matchManagers: ['npm'],
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
|
'@rails/ujs', // Needs to match the major Rails version
|
||||||
'tesseract.js', // Requires code changes
|
'tesseract.js', // Requires code changes
|
||||||
'react-hotkeys', // Requires code changes
|
'react-hotkeys', // Requires code changes
|
||||||
|
|
||||||
|
@ -50,6 +51,12 @@
|
||||||
'sidekiq', // Requires manual upgrade
|
'sidekiq', // Requires manual upgrade
|
||||||
'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
|
'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
|
||||||
'redis', // Requires manual upgrade and sync with Sidekiq version
|
'redis', // Requires manual upgrade and sync with Sidekiq version
|
||||||
|
'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964
|
||||||
|
|
||||||
|
// Needs major Rails version bump
|
||||||
|
'rack',
|
||||||
|
'rails',
|
||||||
|
'rails-i18n',
|
||||||
],
|
],
|
||||||
matchUpdateTypes: ['major'],
|
matchUpdateTypes: ['major'],
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
|
@ -2,8 +2,6 @@ name: Build container image
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -16,7 +14,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-image:
|
build-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker-builder
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
@ -49,10 +47,8 @@ jobs:
|
||||||
images: |
|
images: |
|
||||||
tootsuite/mastodon
|
tootsuite/mastodon
|
||||||
ghcr.io/mastodon/mastodon
|
ghcr.io/mastodon/mastodon
|
||||||
# Only tag with latest when ran against the latest stable branch
|
|
||||||
# This needs to be updated after each minor version release
|
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
|
latest=auto
|
||||||
tags: |
|
tags: |
|
||||||
type=edge,branch=main
|
type=edge,branch=main
|
||||||
type=pep440,pattern={{raw}}
|
type=pep440,pattern={{raw}}
|
||||||
|
|
|
@ -9,7 +9,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-nightly-image:
|
build-nightly-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker-builder
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
@ -37,6 +37,7 @@ jobs:
|
||||||
latest=auto
|
latest=auto
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=nightly
|
type=raw,value=nightly
|
||||||
|
type=raw,value=edge
|
||||||
type=schedule,pattern=nightly-{{date 'YYYY-MM-DD' tz='Etc/UTC'}}
|
type=schedule,pattern=nightly-{{date 'YYYY-MM-DD' tz='Etc/UTC'}}
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.description=Nightly build image used for testing purposes
|
org.opencontainers.image.description=Nightly build image used for testing purposes
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
name: Bundler Audit
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- 'dependabot/**'
|
|
||||||
paths:
|
|
||||||
- 'Gemfile*'
|
|
||||||
- '.ruby-version'
|
|
||||||
- '.bundler-audit.yml'
|
|
||||||
- '.github/workflows/bundler-audit.yml'
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'Gemfile*'
|
|
||||||
- '.ruby-version'
|
|
||||||
- '.bundler-audit.yml'
|
|
||||||
- '.github/workflows/bundler-audit.yml'
|
|
||||||
|
|
||||||
schedule:
|
|
||||||
- cron: '0 5 * * 1'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
security:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
|
||||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
|
||||||
|
|
||||||
- name: Set up Ruby
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
ruby-version: .ruby-version
|
|
||||||
bundler-cache: true
|
|
||||||
|
|
||||||
- name: Run bundler-audit
|
|
||||||
run: bundle exec bundler-audit
|
|
|
@ -8,7 +8,7 @@ on:
|
||||||
- 'Gemfile*'
|
- 'Gemfile*'
|
||||||
- '.rubocop*.yml'
|
- '.rubocop*.yml'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
- 'config/brakeman.ignore'
|
- '.bundler-audit.yml'
|
||||||
- '**/*.rb'
|
- '**/*.rb'
|
||||||
- '**/*.rake'
|
- '**/*.rake'
|
||||||
- '.github/workflows/lint-ruby.yml'
|
- '.github/workflows/lint-ruby.yml'
|
||||||
|
@ -18,7 +18,7 @@ on:
|
||||||
- 'Gemfile*'
|
- 'Gemfile*'
|
||||||
- '.rubocop*.yml'
|
- '.rubocop*.yml'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
- 'config/brakeman.ignore'
|
- '.bundler-audit.yml'
|
||||||
- '**/*.rb'
|
- '**/*.rb'
|
||||||
- '**/*.rake'
|
- '**/*.rake'
|
||||||
- '.github/workflows/lint-ruby.yml'
|
- '.github/workflows/lint-ruby.yml'
|
||||||
|
@ -46,6 +46,5 @@ jobs:
|
||||||
- name: Run rubocop
|
- name: Run rubocop
|
||||||
run: bundle exec rubocop
|
run: bundle exec rubocop
|
||||||
|
|
||||||
- name: Run brakeman
|
- name: Run bundler-audit
|
||||||
if: always() # Run both checks, even if the first failed
|
run: bundle exec bundler-audit
|
||||||
run: bundle exec brakeman
|
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
name: PR Needs Rebase
|
name: PR Needs Rebase
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
push:
|
||||||
- cron: '0 * * * *'
|
branches-ignore:
|
||||||
|
- 'dependabot/**'
|
||||||
|
- 'renovate/**'
|
||||||
|
- 'l10n_main'
|
||||||
|
pull_request_target:
|
||||||
|
branches-ignore:
|
||||||
|
- 'dependabot/**'
|
||||||
|
- 'renovate/**'
|
||||||
|
- 'l10n_main'
|
||||||
|
types: [synchronize]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
|
@ -1,23 +1,73 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `haml-lint --auto-gen-config`
|
# `haml-lint --auto-gen-config`
|
||||||
# on 2023-07-11 23:58:05 +0200 using Haml-Lint version 0.48.0.
|
# on 2023-03-15 00:55:01 -0400 using Haml-Lint version 0.45.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: 94
|
# Offense count: 63
|
||||||
RuboCop:
|
RuboCop:
|
||||||
enabled: false
|
exclude:
|
||||||
|
- 'app/views/accounts/_og.html.haml'
|
||||||
|
- 'app/views/admin/account_warnings/_account_warning.html.haml'
|
||||||
|
- 'app/views/admin/accounts/index.html.haml'
|
||||||
|
- 'app/views/admin/accounts/show.html.haml'
|
||||||
|
- 'app/views/admin/announcements/edit.html.haml'
|
||||||
|
- 'app/views/admin/announcements/new.html.haml'
|
||||||
|
- 'app/views/admin/disputes/appeals/_appeal.html.haml'
|
||||||
|
- 'app/views/admin/domain_blocks/edit.html.haml'
|
||||||
|
- 'app/views/admin/domain_blocks/new.html.haml'
|
||||||
|
- 'app/views/admin/ip_blocks/new.html.haml'
|
||||||
|
- 'app/views/admin/reports/actions/preview.html.haml'
|
||||||
|
- 'app/views/admin/reports/index.html.haml'
|
||||||
|
- 'app/views/admin/reports/show.html.haml'
|
||||||
|
- 'app/views/admin/roles/_form.html.haml'
|
||||||
|
- 'app/views/admin/settings/about/show.html.haml'
|
||||||
|
- 'app/views/admin/settings/appearance/show.html.haml'
|
||||||
|
- 'app/views/admin/settings/registrations/show.html.haml'
|
||||||
|
- 'app/views/admin/statuses/show.html.haml'
|
||||||
|
- 'app/views/auth/registrations/new.html.haml'
|
||||||
|
- 'app/views/disputes/strikes/show.html.haml'
|
||||||
|
- 'app/views/filters/_filter_fields.html.haml'
|
||||||
|
- 'app/views/invites/_form.html.haml'
|
||||||
|
- 'app/views/layouts/application.html.haml'
|
||||||
|
- 'app/views/layouts/error.html.haml'
|
||||||
|
- 'app/views/notification_mailer/_status.html.haml'
|
||||||
|
- 'app/views/settings/applications/_fields.html.haml'
|
||||||
|
- 'app/views/settings/imports/show.html.haml'
|
||||||
|
- 'app/views/settings/preferences/appearance/show.html.haml'
|
||||||
|
- 'app/views/settings/preferences/other/show.html.haml'
|
||||||
|
- 'app/views/statuses/_detailed_status.html.haml'
|
||||||
|
- 'app/views/statuses/_poll.html.haml'
|
||||||
|
- 'app/views/statuses/show.html.haml'
|
||||||
|
- 'app/views/statuses_cleanup/show.html.haml'
|
||||||
|
- 'app/views/user_mailer/warning.html.haml'
|
||||||
|
|
||||||
# Offense count: 960
|
# Offense count: 913
|
||||||
LineLength:
|
LineLength:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
# Offense count: 22
|
# Offense count: 22
|
||||||
UnnecessaryStringOutput:
|
UnnecessaryStringOutput:
|
||||||
enabled: false
|
exclude:
|
||||||
|
- 'app/views/accounts/show.html.haml'
|
||||||
|
- 'app/views/admin/custom_emojis/_custom_emoji.html.haml'
|
||||||
|
- 'app/views/admin/relays/_relay.html.haml'
|
||||||
|
- 'app/views/admin/rules/_rule.html.haml'
|
||||||
|
- 'app/views/admin/statuses/index.html.haml'
|
||||||
|
- 'app/views/auth/registrations/_sessions.html.haml'
|
||||||
|
- 'app/views/disputes/strikes/show.html.haml'
|
||||||
|
- 'app/views/notification_mailer/_status.html.haml'
|
||||||
|
- 'app/views/settings/two_factor_authentication_methods/index.html.haml'
|
||||||
|
- 'app/views/statuses/_detailed_status.html.haml'
|
||||||
|
- 'app/views/statuses/_poll.html.haml'
|
||||||
|
- 'app/views/statuses/_simple_status.html.haml'
|
||||||
|
- 'app/views/user_mailer/suspicious_sign_in.html.haml'
|
||||||
|
- 'app/views/user_mailer/webauthn_credential_added.html.haml'
|
||||||
|
- 'app/views/user_mailer/webauthn_credential_deleted.html.haml'
|
||||||
|
- 'app/views/user_mailer/welcome.html.haml'
|
||||||
|
|
||||||
# Offense count: 3
|
# Offense count: 3
|
||||||
ViewLength:
|
ViewLength:
|
||||||
|
|
|
@ -24,6 +24,7 @@ AllCops:
|
||||||
Exclude:
|
Exclude:
|
||||||
- db/schema.rb
|
- db/schema.rb
|
||||||
- 'bin/*'
|
- 'bin/*'
|
||||||
|
- 'Rakefile'
|
||||||
- 'node_modules/**/*'
|
- 'node_modules/**/*'
|
||||||
- 'Vagrantfile'
|
- 'Vagrantfile'
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
|
@ -191,11 +192,6 @@ Style/RedundantBegin:
|
||||||
Style/RescueStandardError:
|
Style/RescueStandardError:
|
||||||
EnforcedStyle: implicit
|
EnforcedStyle: implicit
|
||||||
|
|
||||||
# Reason: Simplify some spec layouts
|
|
||||||
# https://docs.rubocop.org/rubocop/cops_style.html#stylesemicolon
|
|
||||||
Style/Semicolon:
|
|
||||||
AllowAsExpressionSeparator: true
|
|
||||||
|
|
||||||
# Reason: Originally disabled for CodeClimate, and no config consensus has been found
|
# Reason: Originally disabled for CodeClimate, and no config consensus has been found
|
||||||
# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray
|
# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray
|
||||||
Style/SymbolArray:
|
Style/SymbolArray:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
||||||
# using RuboCop version 1.54.1.
|
# using RuboCop version 1.52.1.
|
||||||
# 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 offenses are removed from the code base.
|
# one by one as the offenses 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
|
||||||
|
@ -28,6 +28,7 @@ Layout/ArgumentAlignment:
|
||||||
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
||||||
Layout/HashAlignment:
|
Layout/HashAlignment:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
- 'config/boot.rb'
|
||||||
- 'config/environments/production.rb'
|
- 'config/environments/production.rb'
|
||||||
- 'config/initializers/rack_attack.rb'
|
- 'config/initializers/rack_attack.rb'
|
||||||
- 'config/routes.rb'
|
- 'config/routes.rb'
|
||||||
|
@ -47,6 +48,15 @@ Layout/SpaceInLambdaLiteral:
|
||||||
- 'config/environments/production.rb'
|
- 'config/environments/production.rb'
|
||||||
- 'config/initializers/content_security_policy.rb'
|
- 'config/initializers/content_security_policy.rb'
|
||||||
|
|
||||||
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||||
|
Lint/AmbiguousBlockAssociation:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
|
||||||
|
- 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb'
|
||||||
|
- 'spec/services/activitypub/process_status_update_service_spec.rb'
|
||||||
|
- 'spec/services/post_status_service_spec.rb'
|
||||||
|
|
||||||
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
||||||
Lint/EmptyBlock:
|
Lint/EmptyBlock:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -96,6 +106,11 @@ Lint/OrAssignmentToConstant:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/sanitize_ext/sanitize_config.rb'
|
- 'lib/sanitize_ext/sanitize_config.rb'
|
||||||
|
|
||||||
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
|
Lint/SendWithMixinArgument:
|
||||||
|
Exclude:
|
||||||
|
- 'config/application.rb'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
|
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
|
||||||
Lint/UnusedBlockArgument:
|
Lint/UnusedBlockArgument:
|
||||||
|
@ -150,6 +165,10 @@ Metrics/CyclomaticComplexity:
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 27
|
Max: 27
|
||||||
|
|
||||||
|
Naming/AccessorMethodName:
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/auth/sessions_controller.rb'
|
||||||
|
|
||||||
# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms.
|
# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms.
|
||||||
# CheckDefinitionPathHierarchyRoots: lib, spec, test, src
|
# CheckDefinitionPathHierarchyRoots: lib, spec, test, src
|
||||||
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
|
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
|
||||||
|
@ -157,6 +176,19 @@ Naming/FileName:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/locales/sr-Latn.rb'
|
- 'config/locales/sr-Latn.rb'
|
||||||
|
|
||||||
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
|
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
||||||
|
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
|
||||||
|
Naming/MemoizedInstanceVariableName:
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/api/v1/bookmarks_controller.rb'
|
||||||
|
- 'app/controllers/api/v1/favourites_controller.rb'
|
||||||
|
- 'app/controllers/concerns/rate_limit_headers.rb'
|
||||||
|
- 'app/lib/activitypub/activity.rb'
|
||||||
|
- 'app/services/resolve_url_service.rb'
|
||||||
|
- 'app/services/search_service.rb'
|
||||||
|
- 'config/initializers/rack_attack.rb'
|
||||||
|
|
||||||
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
||||||
# SupportedStyles: snake_case, normalcase, non_integer
|
# SupportedStyles: snake_case, normalcase, non_integer
|
||||||
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
||||||
|
@ -251,6 +283,7 @@ RSpec/HookArgument:
|
||||||
- 'spec/serializers/activitypub/note_serializer_spec.rb'
|
- 'spec/serializers/activitypub/note_serializer_spec.rb'
|
||||||
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
|
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
|
||||||
- 'spec/services/import_service_spec.rb'
|
- 'spec/services/import_service_spec.rb'
|
||||||
|
- 'spec/spec_helper.rb'
|
||||||
|
|
||||||
# Configuration parameters: AssignmentOnly.
|
# Configuration parameters: AssignmentOnly.
|
||||||
RSpec/InstanceVariable:
|
RSpec/InstanceVariable:
|
||||||
|
@ -365,6 +398,45 @@ RSpec/PendingWithoutReason:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'spec/models/account_spec.rb'
|
- 'spec/models/account_spec.rb'
|
||||||
|
|
||||||
|
RSpec/StubbedMock:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/controllers/api/base_controller_spec.rb'
|
||||||
|
- 'spec/controllers/api/v1/media_controller_spec.rb'
|
||||||
|
- 'spec/controllers/auth/registrations_controller_spec.rb'
|
||||||
|
- 'spec/helpers/application_helper_spec.rb'
|
||||||
|
- 'spec/lib/status_filter_spec.rb'
|
||||||
|
- 'spec/lib/status_finder_spec.rb'
|
||||||
|
- 'spec/lib/webfinger_resource_spec.rb'
|
||||||
|
- 'spec/services/activitypub/process_collection_service_spec.rb'
|
||||||
|
|
||||||
|
RSpec/SubjectDeclaration:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/controllers/admin/domain_blocks_controller_spec.rb'
|
||||||
|
- 'spec/models/account_migration_spec.rb'
|
||||||
|
- 'spec/models/account_spec.rb'
|
||||||
|
- 'spec/models/relationship_filter_spec.rb'
|
||||||
|
- 'spec/models/user_role_spec.rb'
|
||||||
|
- 'spec/policies/account_moderation_note_policy_spec.rb'
|
||||||
|
- 'spec/policies/account_policy_spec.rb'
|
||||||
|
- 'spec/policies/backup_policy_spec.rb'
|
||||||
|
- 'spec/policies/custom_emoji_policy_spec.rb'
|
||||||
|
- 'spec/policies/domain_block_policy_spec.rb'
|
||||||
|
- 'spec/policies/email_domain_block_policy_spec.rb'
|
||||||
|
- 'spec/policies/instance_policy_spec.rb'
|
||||||
|
- 'spec/policies/invite_policy_spec.rb'
|
||||||
|
- 'spec/policies/relay_policy_spec.rb'
|
||||||
|
- 'spec/policies/report_note_policy_spec.rb'
|
||||||
|
- 'spec/policies/report_policy_spec.rb'
|
||||||
|
- 'spec/policies/settings_policy_spec.rb'
|
||||||
|
- 'spec/policies/tag_policy_spec.rb'
|
||||||
|
- 'spec/policies/user_policy_spec.rb'
|
||||||
|
- 'spec/services/activitypub/process_account_service_spec.rb'
|
||||||
|
|
||||||
|
RSpec/SubjectStub:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/services/unallow_domain_service_spec.rb'
|
||||||
|
- 'spec/validators/blacklisted_email_validator_spec.rb'
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -704,6 +776,406 @@ Style/FormatStringToken:
|
||||||
- 'config/initializers/devise.rb'
|
- 'config/initializers/devise.rb'
|
||||||
- 'lib/paperclip/color_extractor.rb'
|
- 'lib/paperclip/color_extractor.rb'
|
||||||
|
|
||||||
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
# SupportedStyles: always, always_true, never
|
||||||
|
Style/FrozenStringLiteralComment:
|
||||||
|
Exclude:
|
||||||
|
- 'app/views/accounts/show.rss.ruby'
|
||||||
|
- 'app/views/tags/show.rss.ruby'
|
||||||
|
- 'app/views/well_known/host_meta/show.xml.ruby'
|
||||||
|
- 'config/application.rb'
|
||||||
|
- 'config/boot.rb'
|
||||||
|
- 'config/environment.rb'
|
||||||
|
- 'config/environments/development.rb'
|
||||||
|
- 'config/environments/production.rb'
|
||||||
|
- 'config/environments/test.rb'
|
||||||
|
- 'config/initializers/0_post_deployment_migrations.rb'
|
||||||
|
- 'config/initializers/active_model_serializers.rb'
|
||||||
|
- 'config/initializers/application_controller_renderer.rb'
|
||||||
|
- 'config/initializers/assets.rb'
|
||||||
|
- 'config/initializers/backtrace_silencers.rb'
|
||||||
|
- 'config/initializers/cache_logging.rb'
|
||||||
|
- 'config/initializers/chewy.rb'
|
||||||
|
- 'config/initializers/content_security_policy.rb'
|
||||||
|
- 'config/initializers/cookies_serializer.rb'
|
||||||
|
- 'config/initializers/cors.rb'
|
||||||
|
- 'config/initializers/devise.rb'
|
||||||
|
- 'config/initializers/doorkeeper.rb'
|
||||||
|
- 'config/initializers/fast_blank.rb'
|
||||||
|
- 'config/initializers/ffmpeg.rb'
|
||||||
|
- 'config/initializers/filter_parameter_logging.rb'
|
||||||
|
- 'config/initializers/http_client_proxy.rb'
|
||||||
|
- 'config/initializers/httplog.rb'
|
||||||
|
- 'config/initializers/inflections.rb'
|
||||||
|
- 'config/initializers/mail_delivery_job.rb'
|
||||||
|
- 'config/initializers/makara.rb'
|
||||||
|
- 'config/initializers/mime_types.rb'
|
||||||
|
- 'config/initializers/oj.rb'
|
||||||
|
- 'config/initializers/omniauth.rb'
|
||||||
|
- 'config/initializers/open_uri_redirection.rb'
|
||||||
|
- 'config/initializers/permissions_policy.rb'
|
||||||
|
- 'config/initializers/pghero.rb'
|
||||||
|
- 'config/initializers/preload_link_headers.rb'
|
||||||
|
- 'config/initializers/premailer_rails.rb'
|
||||||
|
- 'config/initializers/rack_attack_logging.rb'
|
||||||
|
- 'config/initializers/redis.rb'
|
||||||
|
- 'config/initializers/session_store.rb'
|
||||||
|
- 'config/initializers/simple_form.rb'
|
||||||
|
- 'config/initializers/stoplight.rb'
|
||||||
|
- 'config/initializers/trusted_proxies.rb'
|
||||||
|
- 'config/initializers/twitter_regex.rb'
|
||||||
|
- 'config/initializers/webauthn.rb'
|
||||||
|
- 'config/initializers/wrap_parameters.rb'
|
||||||
|
- 'config/locales/sr-Latn.rb'
|
||||||
|
- 'config/locales/sr.rb'
|
||||||
|
- 'config/puma.rb'
|
||||||
|
- 'db/migrate/20160220174730_create_accounts.rb'
|
||||||
|
- 'db/migrate/20160220211917_create_statuses.rb'
|
||||||
|
- 'db/migrate/20160221003140_create_users.rb'
|
||||||
|
- 'db/migrate/20160221003621_create_follows.rb'
|
||||||
|
- 'db/migrate/20160222122600_create_stream_entries.rb'
|
||||||
|
- 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb'
|
||||||
|
- 'db/migrate/20160223162837_add_metadata_to_statuses.rb'
|
||||||
|
- 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb'
|
||||||
|
- 'db/migrate/20160223165723_add_url_to_statuses.rb'
|
||||||
|
- 'db/migrate/20160223165855_add_url_to_accounts.rb'
|
||||||
|
- 'db/migrate/20160223171800_create_favourites.rb'
|
||||||
|
- 'db/migrate/20160224223247_create_mentions.rb'
|
||||||
|
- 'db/migrate/20160227230233_add_attachment_avatar_to_accounts.rb'
|
||||||
|
- 'db/migrate/20160305115639_add_devise_to_users.rb'
|
||||||
|
- 'db/migrate/20160306172223_create_doorkeeper_tables.rb'
|
||||||
|
- 'db/migrate/20160312193225_add_attachment_header_to_accounts.rb'
|
||||||
|
- 'db/migrate/20160314164231_add_owner_to_application.rb'
|
||||||
|
- 'db/migrate/20160316103650_add_missing_indices.rb'
|
||||||
|
- 'db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb'
|
||||||
|
- 'db/migrate/20160325130944_add_admin_to_users.rb'
|
||||||
|
- 'db/migrate/20160826155805_add_superapp_to_oauth_applications.rb'
|
||||||
|
- 'db/migrate/20160905150353_create_media_attachments.rb'
|
||||||
|
- 'db/migrate/20160919221059_add_subscription_expires_at_to_accounts.rb'
|
||||||
|
- 'db/migrate/20160920003904_remove_verify_token_from_accounts.rb'
|
||||||
|
- 'db/migrate/20160926213048_remove_owner_from_application.rb'
|
||||||
|
- 'db/migrate/20161003142332_add_confirmable_to_users.rb'
|
||||||
|
- 'db/migrate/20161003145426_create_blocks.rb'
|
||||||
|
- 'db/migrate/20161006213403_rails_settings_migration.rb'
|
||||||
|
- 'db/migrate/20161009120834_create_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20161027172456_add_silenced_to_accounts.rb'
|
||||||
|
- 'db/migrate/20161104173623_create_tags.rb'
|
||||||
|
- 'db/migrate/20161105130633_create_statuses_tags_join_table.rb'
|
||||||
|
- 'db/migrate/20161116162355_add_locale_to_users.rb'
|
||||||
|
- 'db/migrate/20161119211120_create_notifications.rb'
|
||||||
|
- 'db/migrate/20161122163057_remove_unneeded_indexes.rb'
|
||||||
|
- 'db/migrate/20161123093447_add_sensitive_to_statuses.rb'
|
||||||
|
- 'db/migrate/20161128103007_create_subscriptions.rb'
|
||||||
|
- 'db/migrate/20161130142058_add_last_successful_delivery_at_to_subscriptions.rb'
|
||||||
|
- 'db/migrate/20161130185319_add_visibility_to_statuses.rb'
|
||||||
|
- 'db/migrate/20161202132159_add_in_reply_to_account_id_to_statuses.rb'
|
||||||
|
- 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb'
|
||||||
|
- 'db/migrate/20161205214545_add_suspended_to_accounts.rb'
|
||||||
|
- 'db/migrate/20161221152630_add_hidden_to_stream_entries.rb'
|
||||||
|
- 'db/migrate/20161222201034_add_locked_to_accounts.rb'
|
||||||
|
- 'db/migrate/20161222204147_create_follow_requests.rb'
|
||||||
|
- 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb'
|
||||||
|
- 'db/migrate/20170109120109_create_web_settings.rb'
|
||||||
|
- 'db/migrate/20170112154826_migrate_settings.rb'
|
||||||
|
- 'db/migrate/20170114194937_add_application_to_statuses.rb'
|
||||||
|
- 'db/migrate/20170114203041_add_website_to_oauth_application.rb'
|
||||||
|
- 'db/migrate/20170119214911_create_preview_cards.rb'
|
||||||
|
- 'db/migrate/20170123162658_add_severity_to_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20170125145934_add_spoiler_text_to_statuses.rb'
|
||||||
|
- 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb'
|
||||||
|
- 'db/migrate/20170205175257_remove_devices.rb'
|
||||||
|
- 'db/migrate/20170209184350_add_reply_to_statuses.rb'
|
||||||
|
- 'db/migrate/20170214110202_create_reports.rb'
|
||||||
|
- 'db/migrate/20170217012631_add_reblog_of_id_foreign_key_to_statuses.rb'
|
||||||
|
- 'db/migrate/20170301222600_create_mutes.rb'
|
||||||
|
- 'db/migrate/20170303212857_add_last_emailed_at_to_users.rb'
|
||||||
|
- 'db/migrate/20170304202101_add_type_to_media_attachments.rb'
|
||||||
|
- 'db/migrate/20170317193015_add_search_index_to_accounts.rb'
|
||||||
|
- 'db/migrate/20170318214217_add_header_remote_url_to_accounts.rb'
|
||||||
|
- 'db/migrate/20170322021028_add_lowercase_index_to_accounts.rb'
|
||||||
|
- 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb'
|
||||||
|
- 'db/migrate/20170322162804_add_search_index_to_tags.rb'
|
||||||
|
- 'db/migrate/20170330021336_add_counter_caches.rb'
|
||||||
|
- 'db/migrate/20170330163835_create_imports.rb'
|
||||||
|
- 'db/migrate/20170330164118_add_attachment_data_to_imports.rb'
|
||||||
|
- 'db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb'
|
||||||
|
- 'db/migrate/20170405112956_add_index_on_mentions_status_id.rb'
|
||||||
|
- 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb'
|
||||||
|
- 'db/migrate/20170409170753_add_last_webfingered_at_to_accounts.rb'
|
||||||
|
- 'db/migrate/20170414080609_add_devise_two_factor_backupable_to_users.rb'
|
||||||
|
- 'db/migrate/20170414132105_add_language_to_statuses.rb'
|
||||||
|
- 'db/migrate/20170418160728_add_indexes_to_reports_for_accounts.rb'
|
||||||
|
- 'db/migrate/20170423005413_add_allowed_languages_to_user.rb'
|
||||||
|
- 'db/migrate/20170424003227_create_account_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20170424112722_add_status_id_index_to_statuses_tags.rb'
|
||||||
|
- 'db/migrate/20170425131920_add_media_attachment_meta.rb'
|
||||||
|
- 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb'
|
||||||
|
- 'db/migrate/20170427011934_re_add_owner_to_application.rb'
|
||||||
|
- 'db/migrate/20170506235850_create_conversations.rb'
|
||||||
|
- 'db/migrate/20170507000211_add_conversation_id_to_statuses.rb'
|
||||||
|
- 'db/migrate/20170507141759_optimize_index_subscriptions.rb'
|
||||||
|
- 'db/migrate/20170508230434_create_conversation_mutes.rb'
|
||||||
|
- 'db/migrate/20170516072309_add_index_accounts_on_uri.rb'
|
||||||
|
- 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb'
|
||||||
|
- 'db/migrate/20170601210557_add_index_on_media_attachments_account_id.rb'
|
||||||
|
- 'db/migrate/20170604144747_add_foreign_keys_for_accounts.rb'
|
||||||
|
- 'db/migrate/20170606113804_change_tag_search_index_to_btree.rb'
|
||||||
|
- 'db/migrate/20170609145826_remove_default_language_from_statuses.rb'
|
||||||
|
- 'db/migrate/20170610000000_add_statuses_index_on_account_id_id.rb'
|
||||||
|
- 'db/migrate/20170623152212_create_session_activations.rb'
|
||||||
|
- 'db/migrate/20170624134742_add_description_to_session_activations.rb'
|
||||||
|
- 'db/migrate/20170625140443_add_access_token_id_to_session_activations.rb'
|
||||||
|
- 'db/migrate/20170711225116_fix_null_booleans.rb'
|
||||||
|
- 'db/migrate/20170713112503_make_tag_search_case_insensitive.rb'
|
||||||
|
- 'db/migrate/20170713175513_create_web_push_subscriptions.rb'
|
||||||
|
- 'db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb'
|
||||||
|
- 'db/migrate/20170714184731_add_domain_to_subscriptions.rb'
|
||||||
|
- 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb'
|
||||||
|
- 'db/migrate/20170718211102_add_activitypub_to_accounts.rb'
|
||||||
|
- 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb'
|
||||||
|
- 'db/migrate/20170823162448_create_status_pins.rb'
|
||||||
|
- 'db/migrate/20170824103029_add_timestamps_to_status_pins.rb'
|
||||||
|
- 'db/migrate/20170829215220_remove_status_pins_account_index.rb'
|
||||||
|
- 'db/migrate/20170901141119_truncate_preview_cards.rb'
|
||||||
|
- 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb'
|
||||||
|
- 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb'
|
||||||
|
- 'db/migrate/20170905165803_add_local_to_statuses.rb'
|
||||||
|
- 'db/migrate/20170913000752_create_site_uploads.rb'
|
||||||
|
- 'db/migrate/20170917153509_create_custom_emojis.rb'
|
||||||
|
- 'db/migrate/20170918125918_ids_to_bigints.rb'
|
||||||
|
- 'db/migrate/20170920024819_status_ids_to_timestamp_ids.rb'
|
||||||
|
- 'db/migrate/20170920032311_fix_reblogs_in_feeds.rb'
|
||||||
|
- 'db/migrate/20170924022025_ids_to_bigints2.rb'
|
||||||
|
- 'db/migrate/20170927215609_add_description_to_media_attachments.rb'
|
||||||
|
- 'db/migrate/20170928082043_create_email_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20171005102658_create_account_moderation_notes.rb'
|
||||||
|
- 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb'
|
||||||
|
- 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb'
|
||||||
|
- 'db/migrate/20171010023049_add_foreign_key_to_account_moderation_notes.rb'
|
||||||
|
- 'db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb'
|
||||||
|
- 'db/migrate/20171020084748_add_visible_in_picker_to_custom_emoji.rb'
|
||||||
|
- 'db/migrate/20171028221157_add_reblogs_to_follows.rb'
|
||||||
|
- 'db/migrate/20171107143332_add_memorial_to_accounts.rb'
|
||||||
|
- 'db/migrate/20171107143624_add_disabled_to_users.rb'
|
||||||
|
- 'db/migrate/20171109012327_add_moderator_to_accounts.rb'
|
||||||
|
- 'db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20171114231651_create_lists.rb'
|
||||||
|
- 'db/migrate/20171116161857_create_list_accounts.rb'
|
||||||
|
- 'db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb'
|
||||||
|
- 'db/migrate/20171119172437_create_admin_action_logs.rb'
|
||||||
|
- 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb'
|
||||||
|
- 'db/migrate/20171125024930_create_invites.rb'
|
||||||
|
- 'db/migrate/20171125031751_add_invite_id_to_users.rb'
|
||||||
|
- 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb'
|
||||||
|
- 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb'
|
||||||
|
- 'db/migrate/20171129172043_add_index_on_stream_entries.rb'
|
||||||
|
- 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb'
|
||||||
|
- 'db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb'
|
||||||
|
- 'db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb'
|
||||||
|
- 'db/migrate/20171226094803_more_faster_index_on_notifications.rb'
|
||||||
|
- 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
|
||||||
|
- 'db/migrate/20180109143959_add_remember_token_to_users.rb'
|
||||||
|
- 'db/migrate/20180204034416_create_identities.rb'
|
||||||
|
- 'db/migrate/20180206000000_change_user_id_nonnullable.rb'
|
||||||
|
- 'db/migrate/20180211015820_create_backups.rb'
|
||||||
|
- 'db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb'
|
||||||
|
- 'db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb'
|
||||||
|
- 'db/migrate/20180402031200_add_assigned_account_id_to_reports.rb'
|
||||||
|
- 'db/migrate/20180402040909_create_report_notes.rb'
|
||||||
|
- 'db/migrate/20180410204633_add_fields_to_accounts.rb'
|
||||||
|
- 'db/migrate/20180416210259_add_uri_to_relationships.rb'
|
||||||
|
- 'db/migrate/20180506221944_add_actor_type_to_accounts.rb'
|
||||||
|
- 'db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb'
|
||||||
|
- 'db/migrate/20180510230049_migrate_web_push_subscriptions.rb'
|
||||||
|
- 'db/migrate/20180528141303_fix_accounts_unique_index.rb'
|
||||||
|
- 'db/migrate/20180608213548_reject_following_blocked_users.rb'
|
||||||
|
- 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb'
|
||||||
|
- 'db/migrate/20180615122121_add_autofollow_to_invites.rb'
|
||||||
|
- 'db/migrate/20180616192031_add_chosen_languages_to_users.rb'
|
||||||
|
- 'db/migrate/20180617162849_remove_unused_indexes.rb'
|
||||||
|
- 'db/migrate/20180628181026_create_custom_filters.rb'
|
||||||
|
- 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb'
|
||||||
|
- 'db/migrate/20180711152640_create_relays.rb'
|
||||||
|
- 'db/migrate/20180808175627_create_account_pins.rb'
|
||||||
|
- 'db/migrate/20180812123222_change_relays_enabled.rb'
|
||||||
|
- 'db/migrate/20180812162710_create_status_stats.rb'
|
||||||
|
- 'db/migrate/20180812173710_copy_status_stats.rb'
|
||||||
|
- 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb'
|
||||||
|
- 'db/migrate/20180831171112_create_bookmarks.rb'
|
||||||
|
- 'db/migrate/20180929222014_create_account_conversations.rb'
|
||||||
|
- 'db/migrate/20181007025445_create_pghero_space_stats.rb'
|
||||||
|
- 'db/migrate/20181010141500_add_silent_to_mentions.rb'
|
||||||
|
- 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20181018205649_add_unread_to_account_conversations.rb'
|
||||||
|
- 'db/migrate/20181024224956_migrate_account_conversations.rb'
|
||||||
|
- 'db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb'
|
||||||
|
- 'db/migrate/20181116165755_create_account_stats.rb'
|
||||||
|
- 'db/migrate/20181116173541_copy_account_stats.rb'
|
||||||
|
- 'db/migrate/20181127130500_identity_id_to_bigint.rb'
|
||||||
|
- 'db/migrate/20181127165847_add_show_replies_to_lists.rb'
|
||||||
|
- 'db/migrate/20181203003808_create_accounts_tags_join_table.rb'
|
||||||
|
- 'db/migrate/20181203021853_add_discoverable_to_accounts.rb'
|
||||||
|
- 'db/migrate/20181204193439_add_last_status_at_to_account_stats.rb'
|
||||||
|
- 'db/migrate/20181204215309_create_account_tag_stats.rb'
|
||||||
|
- 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb'
|
||||||
|
- 'db/migrate/20181213184704_create_account_warnings.rb'
|
||||||
|
- 'db/migrate/20181213185533_create_account_warning_presets.rb'
|
||||||
|
- 'db/migrate/20181219235220_add_created_by_application_id_to_users.rb'
|
||||||
|
- 'db/migrate/20181226021420_add_also_known_as_to_accounts.rb'
|
||||||
|
- 'db/migrate/20190103124649_create_scheduled_statuses.rb'
|
||||||
|
- 'db/migrate/20190103124754_add_scheduled_status_id_to_media_attachments.rb'
|
||||||
|
- 'db/migrate/20190117114553_create_tombstones.rb'
|
||||||
|
- 'db/migrate/20190201012802_add_overwrite_to_imports.rb'
|
||||||
|
- 'db/migrate/20190203180359_create_featured_tags.rb'
|
||||||
|
- 'db/migrate/20190225031541_create_polls.rb'
|
||||||
|
- 'db/migrate/20190225031625_create_poll_votes.rb'
|
||||||
|
- 'db/migrate/20190226003449_add_poll_id_to_statuses.rb'
|
||||||
|
- 'db/migrate/20190304152020_add_uri_to_poll_votes.rb'
|
||||||
|
- 'db/migrate/20190306145741_add_lock_version_to_polls.rb'
|
||||||
|
- 'db/migrate/20190307234537_add_approved_to_users.rb'
|
||||||
|
- 'db/migrate/20190314181829_migrate_open_registrations_setting.rb'
|
||||||
|
- 'db/migrate/20190316190352_create_account_identity_proofs.rb'
|
||||||
|
- 'db/migrate/20190317135723_add_uri_to_reports.rb'
|
||||||
|
- 'db/migrate/20190403141604_add_comment_to_invites.rb'
|
||||||
|
- 'db/migrate/20190409054914_create_user_invite_requests.rb'
|
||||||
|
- 'db/migrate/20190420025523_add_blurhash_to_media_attachments.rb'
|
||||||
|
- 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb'
|
||||||
|
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
|
||||||
|
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
|
||||||
|
- 'db/migrate/20190627222225_create_custom_emoji_categories.rb'
|
||||||
|
- 'db/migrate/20190627222826_add_category_id_to_custom_emojis.rb'
|
||||||
|
- 'db/migrate/20190701022101_add_trust_level_to_accounts.rb'
|
||||||
|
- 'db/migrate/20190705002136_create_domain_allows.rb'
|
||||||
|
- 'db/migrate/20190715164535_add_instance_actor.rb'
|
||||||
|
- 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb'
|
||||||
|
- 'db/migrate/20190729185330_add_score_to_tags.rb'
|
||||||
|
- 'db/migrate/20190805123746_add_capabilities_to_tags.rb'
|
||||||
|
- 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20190815225426_add_last_status_at_to_tags.rb'
|
||||||
|
- 'db/migrate/20190819134503_add_deleted_at_to_statuses.rb'
|
||||||
|
- 'db/migrate/20190820003045_update_statuses_index.rb'
|
||||||
|
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
|
||||||
|
- 'db/migrate/20190901035623_add_max_score_to_tags.rb'
|
||||||
|
- 'db/migrate/20190904222339_create_markers.rb'
|
||||||
|
- 'db/migrate/20190914202517_create_account_migrations.rb'
|
||||||
|
- 'db/migrate/20190915194355_create_account_aliases.rb'
|
||||||
|
- 'db/migrate/20190927232842_add_voters_count_to_polls.rb'
|
||||||
|
- 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb'
|
||||||
|
- 'db/migrate/20191007013357_update_pt_locales.rb'
|
||||||
|
- 'db/migrate/20191031163205_change_list_account_follow_nullable.rb'
|
||||||
|
- 'db/migrate/20191212003415_increase_backup_size.rb'
|
||||||
|
- 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb'
|
||||||
|
- 'db/migrate/20191218153258_create_announcements.rb'
|
||||||
|
- 'db/migrate/20200113125135_create_announcement_mutes.rb'
|
||||||
|
- 'db/migrate/20200114113335_create_announcement_reactions.rb'
|
||||||
|
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
|
||||||
|
- 'db/migrate/20200126203551_add_published_at_to_announcements.rb'
|
||||||
|
- 'db/migrate/20200306035625_add_processing_to_media_attachments.rb'
|
||||||
|
- 'db/migrate/20200309150742_add_forwarded_to_reports.rb'
|
||||||
|
- 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb'
|
||||||
|
- 'db/migrate/20200312162302_add_status_ids_to_announcements.rb'
|
||||||
|
- 'db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20200317021758_add_expires_at_to_mutes.rb'
|
||||||
|
- 'db/migrate/20200407201300_create_unavailable_domains.rb'
|
||||||
|
- 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
|
||||||
|
- 'db/migrate/20200417125749_add_storage_schema_version.rb'
|
||||||
|
- 'db/migrate/20200508212852_reset_unique_jobs_locks.rb'
|
||||||
|
- 'db/migrate/20200510110808_reset_web_app_secret.rb'
|
||||||
|
- 'db/migrate/20200510181721_remove_duplicated_indexes_pghero.rb'
|
||||||
|
- 'db/migrate/20200516180352_create_devices.rb'
|
||||||
|
- 'db/migrate/20200516183822_create_one_time_keys.rb'
|
||||||
|
- 'db/migrate/20200518083523_create_encrypted_messages.rb'
|
||||||
|
- 'db/migrate/20200521180606_encrypted_message_ids_to_timestamp_ids.rb'
|
||||||
|
- 'db/migrate/20200529214050_add_devices_url_to_accounts.rb'
|
||||||
|
- 'db/migrate/20200601222558_create_system_keys.rb'
|
||||||
|
- 'db/migrate/20200605155027_add_blurhash_to_preview_cards.rb'
|
||||||
|
- 'db/migrate/20200608113046_add_sign_in_token_to_users.rb'
|
||||||
|
- 'db/migrate/20200614002136_add_sensitized_to_accounts.rb'
|
||||||
|
- 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb'
|
||||||
|
- 'db/migrate/20200622213645_media_attachment_ids_to_timestamp_ids.rb'
|
||||||
|
- 'db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb'
|
||||||
|
- 'db/migrate/20200628133322_create_account_notes.rb'
|
||||||
|
- 'db/migrate/20200630190240_create_webauthn_credentials.rb'
|
||||||
|
- 'db/migrate/20200630190544_add_webauthn_id_to_users.rb'
|
||||||
|
- 'db/migrate/20200908193330_create_account_deletion_requests.rb'
|
||||||
|
- 'db/migrate/20200917192924_add_notify_to_follows.rb'
|
||||||
|
- 'db/migrate/20200917193034_add_type_to_notifications.rb'
|
||||||
|
- 'db/migrate/20200917222316_add_index_notifications_on_type.rb'
|
||||||
|
- 'db/migrate/20201008202037_create_ip_blocks.rb'
|
||||||
|
- 'db/migrate/20201008220312_add_sign_up_ip_to_users.rb'
|
||||||
|
- 'db/migrate/20201017233919_add_suspension_origin_to_accounts.rb'
|
||||||
|
- 'db/migrate/20201206004238_create_instances.rb'
|
||||||
|
- 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20210221045109_create_rules.rb'
|
||||||
|
- 'db/migrate/20210306164523_account_ids_to_timestamp_ids.rb'
|
||||||
|
- 'db/migrate/20210322164601_create_account_summaries.rb'
|
||||||
|
- 'db/migrate/20210323114347_create_follow_recommendations.rb'
|
||||||
|
- 'db/migrate/20210324171613_create_follow_recommendation_suppressions.rb'
|
||||||
|
- 'db/migrate/20210416200740_create_canonical_email_blocks.rb'
|
||||||
|
- 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb'
|
||||||
|
- 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb'
|
||||||
|
- 'db/migrate/20210505174616_update_follow_recommendations_to_version_2.rb'
|
||||||
|
- 'db/migrate/20210609202149_create_login_activities.rb'
|
||||||
|
- 'db/migrate/20210616214526_create_user_ips.rb'
|
||||||
|
- 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb'
|
||||||
|
- 'db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb'
|
||||||
|
- 'db/migrate/20210722120340_create_account_statuses_cleanup_policies.rb'
|
||||||
|
- 'db/migrate/20210904215403_add_edited_at_to_statuses.rb'
|
||||||
|
- 'db/migrate/20210908220918_create_status_edits.rb'
|
||||||
|
- 'db/migrate/20211031031021_create_preview_card_providers.rb'
|
||||||
|
- 'db/migrate/20211112011713_add_language_to_preview_cards.rb'
|
||||||
|
- 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb'
|
||||||
|
- 'db/migrate/20211123212714_add_link_type_to_preview_cards.rb'
|
||||||
|
- 'db/migrate/20211213040746_update_account_summaries_to_version_2.rb'
|
||||||
|
- 'db/migrate/20211231080958_add_category_to_reports.rb'
|
||||||
|
- 'db/migrate/20220105163928_remove_mentions_status_id_index.rb'
|
||||||
|
- 'db/migrate/20220115125126_add_report_id_to_account_warnings.rb'
|
||||||
|
- 'db/migrate/20220115125341_fix_account_warning_actions.rb'
|
||||||
|
- 'db/migrate/20220116202951_add_deleted_at_index_on_statuses.rb'
|
||||||
|
- 'db/migrate/20220124141035_create_appeals.rb'
|
||||||
|
- 'db/migrate/20220202200743_add_trendable_to_accounts.rb'
|
||||||
|
- 'db/migrate/20220202200926_add_trendable_to_statuses.rb'
|
||||||
|
- 'db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb'
|
||||||
|
- 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb'
|
||||||
|
- 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb'
|
||||||
|
- 'db/migrate/20220302232632_add_ordered_media_attachment_ids_to_statuses.rb'
|
||||||
|
- 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb'
|
||||||
|
- 'db/migrate/20220304195405_migrate_hide_network_preference.rb'
|
||||||
|
- 'db/migrate/20220307094650_fix_featured_tags_constraints.rb'
|
||||||
|
- 'db/migrate/20220309213005_fix_reblog_deleted_at.rb'
|
||||||
|
- 'db/migrate/20220316233212_update_kurdish_locales.rb'
|
||||||
|
- 'db/migrate/20220428112511_add_index_statuses_on_account_id.rb'
|
||||||
|
- 'db/migrate/20220428112727_add_index_statuses_pins_on_status_id.rb'
|
||||||
|
- 'db/migrate/20220428114454_add_index_reports_on_assigned_account_id.rb'
|
||||||
|
- 'db/migrate/20220428114902_add_index_reports_on_action_taken_by_account_id.rb'
|
||||||
|
- 'db/migrate/20220606044941_create_webhooks.rb'
|
||||||
|
- 'db/migrate/20220611210335_create_user_roles.rb'
|
||||||
|
- 'db/migrate/20220611212541_add_role_id_to_users.rb'
|
||||||
|
- 'db/migrate/20220710102457_add_display_name_to_tags.rb'
|
||||||
|
- 'db/migrate/20220714171049_create_tag_follows.rb'
|
||||||
|
- 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb'
|
||||||
|
- 'db/migrate/20220824233535_create_status_trends.rb'
|
||||||
|
- 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb'
|
||||||
|
- 'db/migrate/20220829192633_add_languages_to_follows.rb'
|
||||||
|
- 'db/migrate/20220829192658_add_languages_to_follow_requests.rb'
|
||||||
|
- 'db/migrate/20221006061337_create_preview_card_trends.rb'
|
||||||
|
- 'db/migrate/20221012181003_add_blurhash_to_site_uploads.rb'
|
||||||
|
- 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb'
|
||||||
|
- 'db/migrate/20221025171544_add_index_ip_blocks_on_ip.rb'
|
||||||
|
- 'db/migrate/20221104133904_add_name_to_featured_tags.rb'
|
||||||
|
- 'db/post_migrate/20190519130537_remove_boosts_widening_audience.rb'
|
||||||
|
- 'db/post_migrate/20210308133107_remove_subscription_expires_at_from_accounts.rb'
|
||||||
|
- 'db/post_migrate/20220118183123_remove_rememberable_from_users.rb'
|
||||||
|
- 'db/seeds/01_web_app.rb'
|
||||||
|
- 'db/seeds/02_instance_actor.rb'
|
||||||
|
- 'db/seeds/03_roles.rb'
|
||||||
|
- 'db/seeds/04_admin.rb'
|
||||||
|
- 'lib/rails/engine_extensions.rb'
|
||||||
|
- 'lib/tasks/branding.rake'
|
||||||
|
- 'spec/fabricators_spec.rb'
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
Style/GlobalStdStream:
|
Style/GlobalStdStream:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -865,6 +1337,13 @@ Style/SafeNavigation:
|
||||||
- 'app/models/concerns/account_finder_concern.rb'
|
- 'app/models/concerns/account_finder_concern.rb'
|
||||||
- 'app/models/status.rb'
|
- 'app/models/status.rb'
|
||||||
|
|
||||||
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
|
# Configuration parameters: AllowAsExpressionSeparator.
|
||||||
|
Style/Semicolon:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/services/activitypub/process_status_update_service_spec.rb'
|
||||||
|
- 'spec/validators/blacklisted_email_validator_spec.rb'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: EnforcedStyle.
|
# Configuration parameters: EnforcedStyle.
|
||||||
# SupportedStyles: only_raise, only_fail, semantic
|
# SupportedStyles: only_raise, only_fail, semantic
|
||||||
|
@ -878,6 +1357,21 @@ Style/SingleArgumentDig:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/webpacker/manifest_extensions.rb'
|
- 'lib/webpacker/manifest_extensions.rb'
|
||||||
|
|
||||||
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
|
Style/SlicingWithRange:
|
||||||
|
Exclude:
|
||||||
|
- 'app/lib/emoji_formatter.rb'
|
||||||
|
- 'app/lib/text_formatter.rb'
|
||||||
|
- 'app/models/account_alias.rb'
|
||||||
|
- 'app/models/domain_block.rb'
|
||||||
|
- 'app/models/email_domain_block.rb'
|
||||||
|
- 'app/models/preview_card_provider.rb'
|
||||||
|
- 'app/validators/status_length_validator.rb'
|
||||||
|
- 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb'
|
||||||
|
- 'lib/active_record/batches.rb'
|
||||||
|
- 'lib/mastodon/premailer_webpack_strategy.rb'
|
||||||
|
- 'lib/tasks/repo.rake'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: EnforcedStyle.
|
# Configuration parameters: EnforcedStyle.
|
||||||
# SupportedStyles: require_parentheses, require_no_parentheses
|
# SupportedStyles: require_parentheses, require_no_parentheses
|
||||||
|
|
58
CHANGELOG.md
58
CHANGELOG.md
|
@ -2,62 +2,6 @@
|
||||||
|
|
||||||
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.1.4] - 2023-07-07
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix branding:generate_app_icons failing because of disallowed ICO coder ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25794))
|
|
||||||
- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796))
|
|
||||||
- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788))
|
|
||||||
|
|
||||||
## [4.1.3] - 2023-07-06
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058))
|
|
||||||
- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868))
|
|
||||||
- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852))
|
|
||||||
- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614))
|
|
||||||
- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510))
|
|
||||||
- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216))
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464))
|
|
||||||
- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519))
|
|
||||||
- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477))
|
|
||||||
- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840))
|
|
||||||
- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361))
|
|
||||||
- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273))
|
|
||||||
- Fix `tootctl accounts approve --number N` not approving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605))
|
|
||||||
- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988))
|
|
||||||
- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015))
|
|
||||||
- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016))
|
|
||||||
- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060))
|
|
||||||
- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713))
|
|
||||||
- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499))
|
|
||||||
- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431))
|
|
||||||
- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637))
|
|
||||||
- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342))
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463))
|
|
||||||
- Update dependencies
|
|
||||||
- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756))
|
|
||||||
- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462)
|
|
||||||
- Fix timeout handling of outbound HTTP requests (CVE-2023-36461)
|
|
||||||
- Fix arbitrary file creation through media processing (CVE-2023-36460)
|
|
||||||
- Fix possible XSS in preview cards (CVE-2023-36459)
|
|
||||||
|
|
||||||
## [4.1.2] - 2023-04-04
|
## [4.1.2] - 2023-04-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -143,7 +87,7 @@ All notable changes to this project will be documented in this file.
|
||||||
- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833))
|
- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833))
|
||||||
- Add setting for status page URL ([Gargron](https://github.com/mastodon/mastodon/pull/23390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23499))
|
- Add setting for status page URL ([Gargron](https://github.com/mastodon/mastodon/pull/23390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23499))
|
||||||
- REST API changes:
|
- REST API changes:
|
||||||
- Add `configuration.urls.status` attribute to the object returned by `GET /api/v2/instance`
|
- Add `configuration.urls.status` attribute to the object returned by `GET /api/v1/instance`
|
||||||
- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938))
|
- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938))
|
||||||
- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131))
|
- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131))
|
||||||
- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895))
|
- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895))
|
||||||
|
|
7
Gemfile
7
Gemfile
|
@ -4,13 +4,14 @@ source 'https://rubygems.org'
|
||||||
ruby '>= 3.0.0'
|
ruby '>= 3.0.0'
|
||||||
|
|
||||||
gem 'puma', '~> 6.3'
|
gem 'puma', '~> 6.3'
|
||||||
gem 'rails', '~> 7.0'
|
gem 'rails', '~> 6.1.7'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 1.2'
|
gem 'thor', '~> 1.2'
|
||||||
gem 'rack', '~> 2.2.7'
|
gem 'rack', '~> 2.2.7'
|
||||||
|
|
||||||
gem 'haml-rails', '~>2.0'
|
gem 'haml-rails', '~>2.0'
|
||||||
gem 'pg', '~> 1.5'
|
gem 'pg', '~> 1.5'
|
||||||
|
gem 'makara', '~> 0.5'
|
||||||
gem 'pghero'
|
gem 'pghero'
|
||||||
gem 'dotenv-rails', '~> 2.8'
|
gem 'dotenv-rails', '~> 2.8'
|
||||||
|
|
||||||
|
@ -66,7 +67,7 @@ gem 'pundit', '~> 2.3'
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'rack-attack', '~> 6.6'
|
gem 'rack-attack', '~> 6.6'
|
||||||
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
|
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
|
||||||
gem 'rails-i18n', '~> 7.0'
|
gem 'rails-i18n', '~> 6.0'
|
||||||
gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true'
|
gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true'
|
||||||
gem 'redcarpet', '~> 3.6'
|
gem 'redcarpet', '~> 3.6'
|
||||||
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
||||||
|
@ -158,7 +159,7 @@ group :development do
|
||||||
gem 'letter_opener_web', '~> 2.0'
|
gem 'letter_opener_web', '~> 2.0'
|
||||||
|
|
||||||
# Security analysis CLI tools
|
# Security analysis CLI tools
|
||||||
gem 'brakeman', '~> 6.0', require: false
|
gem 'brakeman', '~> 5.4', require: false
|
||||||
gem 'bundler-audit', '~> 0.9', require: false
|
gem 'bundler-audit', '~> 0.9', require: false
|
||||||
|
|
||||||
# Linter CLI for HAML files
|
# Linter CLI for HAML files
|
||||||
|
|
155
Gemfile.lock
155
Gemfile.lock
|
@ -18,47 +18,40 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.0.6)
|
actioncable (6.1.7.4)
|
||||||
actionpack (= 7.0.6)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.6)
|
actionmailbox (6.1.7.4)
|
||||||
actionpack (= 7.0.6)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 7.0.6)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 7.0.6)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 7.0.6)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
actionmailer (6.1.7.4)
|
||||||
net-pop
|
actionpack (= 6.1.7.4)
|
||||||
net-smtp
|
actionview (= 6.1.7.4)
|
||||||
actionmailer (7.0.6)
|
activejob (= 6.1.7.4)
|
||||||
actionpack (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
actionview (= 7.0.6)
|
|
||||||
activejob (= 7.0.6)
|
|
||||||
activesupport (= 7.0.6)
|
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
|
||||||
net-pop
|
|
||||||
net-smtp
|
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (7.0.6)
|
actionpack (6.1.7.4)
|
||||||
actionview (= 7.0.6)
|
actionview (= 6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (7.0.6)
|
actiontext (6.1.7.4)
|
||||||
actionpack (= 7.0.6)
|
actionpack (= 6.1.7.4)
|
||||||
activerecord (= 7.0.6)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 7.0.6)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
globalid (>= 0.6.0)
|
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.6)
|
actionview (6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
|
@ -68,26 +61,27 @@ GEM
|
||||||
activemodel (>= 4.1, < 7.1)
|
activemodel (>= 4.1, < 7.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.6)
|
activejob (6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.6)
|
activemodel (6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
activerecord (7.0.6)
|
activerecord (6.1.7.4)
|
||||||
activemodel (= 7.0.6)
|
activemodel (= 6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
activestorage (7.0.6)
|
activestorage (6.1.7.4)
|
||||||
actionpack (= 7.0.6)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 7.0.6)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 7.0.6)
|
activerecord (= 6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (7.0.6)
|
activesupport (6.1.7.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
|
zeitwerk (~> 2.3)
|
||||||
addressable (2.8.4)
|
addressable (2.8.4)
|
||||||
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)
|
||||||
|
@ -136,7 +130,7 @@ GEM
|
||||||
blurhash (0.1.7)
|
blurhash (0.1.7)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (6.0.0)
|
brakeman (5.4.1)
|
||||||
browser (5.3.1)
|
browser (5.3.1)
|
||||||
brpoplpush-redis_script (0.1.3)
|
brpoplpush-redis_script (0.1.3)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
|
@ -152,7 +146,7 @@ GEM
|
||||||
sshkit (>= 1.9.0)
|
sshkit (>= 1.9.0)
|
||||||
capistrano-bundler (2.1.0)
|
capistrano-bundler (2.1.0)
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
capistrano-rails (1.6.3)
|
capistrano-rails (1.6.2)
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
capistrano-bundler (>= 1.1, < 3)
|
capistrano-bundler (>= 1.1, < 3)
|
||||||
capistrano-rbenv (2.2.0)
|
capistrano-rbenv (2.2.0)
|
||||||
|
@ -173,7 +167,7 @@ GEM
|
||||||
activesupport
|
activesupport
|
||||||
cbor (0.5.9.6)
|
cbor (0.5.9.6)
|
||||||
charlock_holmes (0.7.7)
|
charlock_holmes (0.7.7)
|
||||||
chewy (7.3.3)
|
chewy (7.3.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
elasticsearch (>= 7.12.0, < 7.14.0)
|
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
|
@ -297,11 +291,11 @@ GEM
|
||||||
activesupport (>= 5.1)
|
activesupport (>= 5.1)
|
||||||
haml (>= 4.0.6)
|
haml (>= 4.0.6)
|
||||||
railties (>= 5.1)
|
railties (>= 5.1)
|
||||||
haml_lint (0.48.0)
|
haml_lint (0.45.0)
|
||||||
haml (>= 4.0, < 6.2)
|
haml (>= 4.0, < 6.2)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
rubocop (>= 1.0)
|
rubocop (>= 0.50.0)
|
||||||
sysexits (~> 1.1)
|
sysexits (~> 1.1)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
|
@ -379,7 +373,6 @@ GEM
|
||||||
marcel (~> 1.0.1)
|
marcel (~> 1.0.1)
|
||||||
mime-types
|
mime-types
|
||||||
terrapin (~> 0.6.0)
|
terrapin (~> 0.6.0)
|
||||||
language_server-protocol (3.17.0.3)
|
|
||||||
launchy (2.5.2)
|
launchy (2.5.2)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
letter_opener (1.8.1)
|
||||||
|
@ -406,6 +399,8 @@ GEM
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
|
makara (0.5.1)
|
||||||
|
activerecord (>= 5.2.0)
|
||||||
marcel (1.0.2)
|
marcel (1.0.2)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
|
@ -437,7 +432,7 @@ GEM
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.1.0)
|
net-ssh (7.1.0)
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.15.3)
|
nokogiri (1.15.2)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.15.0)
|
oj (3.15.0)
|
||||||
|
@ -515,20 +510,21 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (7.0.6)
|
rails (6.1.7.4)
|
||||||
actioncable (= 7.0.6)
|
actioncable (= 6.1.7.4)
|
||||||
actionmailbox (= 7.0.6)
|
actionmailbox (= 6.1.7.4)
|
||||||
actionmailer (= 7.0.6)
|
actionmailer (= 6.1.7.4)
|
||||||
actionpack (= 7.0.6)
|
actionpack (= 6.1.7.4)
|
||||||
actiontext (= 7.0.6)
|
actiontext (= 6.1.7.4)
|
||||||
actionview (= 7.0.6)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 7.0.6)
|
activejob (= 6.1.7.4)
|
||||||
activemodel (= 7.0.6)
|
activemodel (= 6.1.7.4)
|
||||||
activerecord (= 7.0.6)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 7.0.6)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.6)
|
railties (= 6.1.7.4)
|
||||||
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
|
@ -539,16 +535,15 @@ GEM
|
||||||
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 (6.0.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 7)
|
||||||
railties (7.0.6)
|
railties (6.1.7.4)
|
||||||
actionpack (= 7.0.6)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 7.0.6)
|
activesupport (= 6.1.7.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
zeitwerk (~> 2.5)
|
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
rdf (3.2.11)
|
rdf (3.2.11)
|
||||||
|
@ -596,9 +591,8 @@ GEM
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.12.0)
|
rspec-support (3.12.0)
|
||||||
rspec_chunked (0.6)
|
rspec_chunked (0.6)
|
||||||
rubocop (1.54.1)
|
rubocop (1.52.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.2.3)
|
parser (>= 3.2.2.3)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
@ -616,7 +610,7 @@ GEM
|
||||||
rubocop-performance (1.18.0)
|
rubocop-performance (1.18.0)
|
||||||
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.19.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
|
@ -634,7 +628,7 @@ GEM
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
sanitize (6.0.2)
|
sanitize (6.0.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.7.0)
|
scenic (1.7.0)
|
||||||
|
@ -676,7 +670,7 @@ 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)
|
sshkit (1.21.4)
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
stackprof (0.2.25)
|
stackprof (0.2.25)
|
||||||
|
@ -696,7 +690,7 @@ GEM
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (1.2.2)
|
thor (1.2.2)
|
||||||
tilt (2.2.0)
|
tilt (2.2.0)
|
||||||
timeout (0.4.0)
|
timeout (0.3.2)
|
||||||
tpm-key_attestation (0.12.0)
|
tpm-key_attestation (0.12.0)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
|
@ -773,7 +767,7 @@ DEPENDENCIES
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.16.0)
|
bootsnap (~> 1.16.0)
|
||||||
brakeman (~> 6.0)
|
brakeman (~> 5.4)
|
||||||
browser
|
browser
|
||||||
bundler-audit (~> 0.9)
|
bundler-audit (~> 0.9)
|
||||||
capistrano (~> 3.17)
|
capistrano (~> 3.17)
|
||||||
|
@ -821,6 +815,7 @@ DEPENDENCIES
|
||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
|
makara (~> 0.5)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
mime-types (~> 3.4.1)
|
mime-types (~> 3.4.1)
|
||||||
|
@ -847,9 +842,9 @@ DEPENDENCIES
|
||||||
rack-attack (~> 6.6)
|
rack-attack (~> 6.6)
|
||||||
rack-cors (~> 2.0)
|
rack-cors (~> 2.0)
|
||||||
rack-test (~> 2.1)
|
rack-test (~> 2.1)
|
||||||
rails (~> 7.0)
|
rails (~> 6.1.7)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 7.0)
|
rails-i18n (~> 6.0)
|
||||||
rails-settings-cached (~> 0.6)!
|
rails-settings-cached (~> 0.6)!
|
||||||
rdf-normalize (~> 0.5)
|
rdf-normalize (~> 0.5)
|
||||||
redcarpet (~> 3.6)
|
redcarpet (~> 3.6)
|
||||||
|
|
4
Rakefile
4
Rakefile
|
@ -1,8 +1,6 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||||
|
|
||||||
require File.expand_path('config/application', __dir__)
|
require File.expand_path('../config/application', __FILE__)
|
||||||
|
|
||||||
Rails.application.load_tasks
|
Rails.application.load_tasks
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Api::V1::BookmarksController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
@results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
|
@_results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Api::V1::FavouritesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
@results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
|
@_results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,10 +7,7 @@ class Api::V1::MarkersController < Api::BaseController
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
with_read_replica do
|
|
||||||
@markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline)
|
@markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline)
|
||||||
end
|
|
||||||
|
|
||||||
render json: serialize_map(@markers)
|
render json: serialize_map(@markers)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,8 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
DEFAULT_NOTIFICATIONS_LIMIT = 40
|
DEFAULT_NOTIFICATIONS_LIMIT = 40
|
||||||
|
|
||||||
def index
|
def index
|
||||||
with_read_replica do
|
|
||||||
@notifications = load_notifications
|
@notifications = load_notifications
|
||||||
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
|
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
|
||||||
end
|
|
||||||
|
|
||||||
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -23,6 +23,6 @@ class Api::V1::ReportsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_params
|
def report_params
|
||||||
params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: [])
|
params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,14 +6,11 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
with_read_replica do
|
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
@relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: @statuses,
|
render json: @statuses,
|
||||||
each_serializer: REST::StatusSerializer,
|
each_serializer: REST::StatusSerializer,
|
||||||
relationships: @relationships,
|
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||||
status: account_home_feed.regenerating? ? 206 : 200
|
status: account_home_feed.regenerating? ? 206 : 200
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::Web::EmbedsController < Api::Web::BaseController
|
class Api::Web::EmbedsController < Api::Web::BaseController
|
||||||
include Authorization
|
before_action :require_user!
|
||||||
|
|
||||||
before_action :set_status
|
def create
|
||||||
|
status = StatusFinder.new(params[:url]).status
|
||||||
|
|
||||||
def show
|
return not_found if status.hidden?
|
||||||
return not_found if @status.hidden?
|
|
||||||
|
|
||||||
if @status.local?
|
render json: status, serializer: OEmbedSerializer, width: 400
|
||||||
render json: @status, serializer: OEmbedSerializer, width: 400
|
rescue ActiveRecord::RecordNotFound
|
||||||
else
|
oembed = FetchOEmbedService.new.call(params[:url])
|
||||||
return not_found unless user_signed_in?
|
|
||||||
|
|
||||||
url = ActivityPub::TagManager.instance.url_for(@status)
|
|
||||||
oembed = FetchOEmbedService.new.call(url)
|
|
||||||
return not_found if oembed.nil?
|
return not_found if oembed.nil?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
@ -25,12 +22,4 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
||||||
|
|
||||||
render json: oembed
|
render json: oembed
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def set_status
|
|
||||||
@status = Status.find(params[:id])
|
|
||||||
authorize @status, :show?
|
|
||||||
rescue Mastodon::NotPermittedError
|
|
||||||
not_found
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,6 @@ class ApplicationController < ActionController::Base
|
||||||
include SessionTrackingConcern
|
include SessionTrackingConcern
|
||||||
include CacheConcern
|
include CacheConcern
|
||||||
include DomainControlHelper
|
include DomainControlHelper
|
||||||
include DatabaseHelper
|
|
||||||
|
|
||||||
helper_method :current_account
|
helper_method :current_account
|
||||||
helper_method :current_session
|
helper_method :current_session
|
||||||
|
|
|
@ -124,7 +124,7 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
redirect_to new_user_session_path, alert: I18n.t('devise.failure.timeout')
|
redirect_to new_user_session_path, alert: I18n.t('devise.failure.timeout')
|
||||||
end
|
end
|
||||||
|
|
||||||
def register_attempt_in_session(user)
|
def set_attempt_session(user)
|
||||||
session[:attempt_user_id] = user.id
|
session[:attempt_user_id] = user.id
|
||||||
session[:attempt_user_updated_at] = user.updated_at.to_s
|
session[:attempt_user_updated_at] = user.updated_at.to_s
|
||||||
end
|
end
|
||||||
|
|
|
@ -61,7 +61,7 @@ module RateLimitHeaders
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_time
|
def request_time
|
||||||
@request_time ||= Time.now.utc
|
@_request_time ||= Time.now.utc
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_period_offset
|
def reset_period_offset
|
||||||
|
|
|
@ -75,7 +75,7 @@ module TwoFactorAuthenticationConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def prompt_for_two_factor(user)
|
def prompt_for_two_factor(user)
|
||||||
register_attempt_in_session(user)
|
set_attempt_session(user)
|
||||||
|
|
||||||
@body_classes = 'lighter'
|
@body_classes = 'lighter'
|
||||||
@webauthn_enabled = user.webauthn_enabled?
|
@webauthn_enabled = user.webauthn_enabled?
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DatabaseHelper
|
|
||||||
def with_read_replica(&block)
|
|
||||||
ApplicationRecord.connected_to(role: :read, prevent_writes: true, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_primary(&block)
|
|
||||||
ApplicationRecord.connected_to(role: :primary, &block)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module DomainControlHelper
|
module DomainControlHelper
|
||||||
def domain_not_allowed?(uri_or_domain)
|
def domain_not_allowed?(uri_or_domain)
|
||||||
return false if uri_or_domain.blank?
|
return if uri_or_domain.blank?
|
||||||
|
|
||||||
domain = if uri_or_domain.include?('://')
|
domain = if uri_or_domain.include?('://')
|
||||||
Addressable::URI.parse(uri_or_domain).host
|
Addressable::URI.parse(uri_or_domain).host
|
||||||
|
|
|
@ -54,10 +54,6 @@ module FormattingHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_field_value_format(field, with_rel_me: true)
|
def account_field_value_format(field, with_rel_me: true)
|
||||||
if field.verified? && !field.account.local?
|
|
||||||
TextFormatter.shortened_link(field.value_for_verification)
|
|
||||||
else
|
|
||||||
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,48 +12,52 @@ export const ALERT_DISMISS = 'ALERT_DISMISS';
|
||||||
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
||||||
export const ALERT_NOOP = 'ALERT_NOOP';
|
export const ALERT_NOOP = 'ALERT_NOOP';
|
||||||
|
|
||||||
export const dismissAlert = alert => ({
|
export function dismissAlert(alert) {
|
||||||
|
return {
|
||||||
type: ALERT_DISMISS,
|
type: ALERT_DISMISS,
|
||||||
alert,
|
alert,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const clearAlert = () => ({
|
export function clearAlert() {
|
||||||
|
return {
|
||||||
type: ALERT_CLEAR,
|
type: ALERT_CLEAR,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const showAlert = alert => ({
|
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
|
||||||
|
return {
|
||||||
type: ALERT_SHOW,
|
type: ALERT_SHOW,
|
||||||
alert,
|
title,
|
||||||
});
|
message,
|
||||||
|
message_values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const showAlertForError = (error, skipNotFound = false) => {
|
export function showAlertForError(error, skipNotFound = false) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { data, status, statusText, headers } = error.response;
|
const { data, status, statusText, headers } = error.response;
|
||||||
|
|
||||||
// Skip these errors as they are reflected in the UI
|
|
||||||
if (skipNotFound && (status === 404 || status === 410)) {
|
if (skipNotFound && (status === 404 || status === 410)) {
|
||||||
|
// Skip these errors as they are reflected in the UI
|
||||||
return { type: ALERT_NOOP };
|
return { type: ALERT_NOOP };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limit errors
|
|
||||||
if (status === 429 && headers['x-ratelimit-reset']) {
|
if (status === 429 && headers['x-ratelimit-reset']) {
|
||||||
return showAlert({
|
const reset_date = new Date(headers['x-ratelimit-reset']);
|
||||||
title: messages.rateLimitedTitle,
|
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date });
|
||||||
message: messages.rateLimitedMessage,
|
|
||||||
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return showAlert({
|
let message = statusText;
|
||||||
title: `${status}`,
|
let title = `${status}`;
|
||||||
message: data.error || statusText,
|
|
||||||
});
|
if (data.error) {
|
||||||
|
message = data.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return showAlert(title, message);
|
||||||
|
} else {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
return showAlert();
|
||||||
return showAlert({
|
}
|
||||||
title: messages.unexpectedTitle,
|
|
||||||
message: messages.unexpectedMessage,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,8 +82,6 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
|
||||||
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||||
|
@ -242,13 +240,6 @@ export function submitCompose(routerHistory) {
|
||||||
insertIfOnline('public');
|
insertIfOnline('public');
|
||||||
insertIfOnline(`account:${response.data.account.id}`);
|
insertIfOnline(`account:${response.data.account.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(showAlert({
|
|
||||||
message: messages.published,
|
|
||||||
action: messages.open,
|
|
||||||
dismissAfter: 10000,
|
|
||||||
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
|
||||||
}));
|
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(submitComposeFail(error));
|
dispatch(submitComposeFail(error));
|
||||||
});
|
});
|
||||||
|
@ -281,16 +272,15 @@ export function uploadCompose(files) {
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
||||||
const progress = new Array(files.length).fill(0);
|
const progress = new Array(files.length).fill(0);
|
||||||
|
|
||||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||||
|
|
||||||
if (files.length + media.size + pending > uploadLimit) {
|
if (files.length + media.size + pending > uploadLimit) {
|
||||||
dispatch(showAlert({ message: messages.uploadErrorLimit }));
|
dispatch(showAlert(undefined, messages.uploadErrorLimit));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getState().getIn(['compose', 'poll'])) {
|
if (getState().getIn(['compose', 'poll'])) {
|
||||||
dispatch(showAlert({ message: messages.uploadErrorPoll }));
|
dispatch(showAlert(undefined, messages.uploadErrorPoll));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,9 +86,10 @@ const DIGIT_CHARACTERS = [
|
||||||
|
|
||||||
export const decode83 = (str: string) => {
|
export const decode83 = (str: string) => {
|
||||||
let value = 0;
|
let value = 0;
|
||||||
let digit;
|
let c, digit;
|
||||||
|
|
||||||
for (const c of str) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
c = str[i];
|
||||||
digit = DIGIT_CHARACTERS.indexOf(c);
|
digit = DIGIT_CHARACTERS.indexOf(c);
|
||||||
value = value * 83 + digit;
|
value = value * 83 + digit;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,15 @@ import { Link } from 'react-router-dom';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
import { counterRenderer } from 'mastodon/components/common_counter';
|
||||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
import { EmptyAccount } from 'mastodon/components/empty_account';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||||
|
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
import { Avatar } from './avatar';
|
||||||
import Button from './button';
|
import Button from './button';
|
||||||
import { FollowersCounter } from './counters';
|
|
||||||
import { DisplayName } from './display_name';
|
import { DisplayName } from './display_name';
|
||||||
import { IconButton } from './icon_button';
|
import { IconButton } from './icon_button';
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
import { RelativeTimestamp } from './relative_timestamp';
|
||||||
|
@ -160,7 +160,7 @@ class Account extends ImmutablePureComponent {
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
{!minimal && (
|
{!minimal && (
|
||||||
<div className='account__details'>
|
<div className='account__details'>
|
||||||
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
|
<ShortNumber value={account.get('followers_count')} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { TransitionMotion, spring } from 'react-motion';
|
||||||
|
|
||||||
import { reduceMotion } from '../initial_state';
|
import { reduceMotion } from '../initial_state';
|
||||||
|
|
||||||
import { ShortNumber } from './short_number';
|
import ShortNumber from './short_number';
|
||||||
|
|
||||||
const obfuscatedCount = (count: number) => {
|
const obfuscatedCount = (count: number) => {
|
||||||
if (count < 0) {
|
if (count < 0) {
|
||||||
|
@ -32,7 +32,7 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
|
||||||
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
|
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
|
||||||
const willLeave = useCallback(
|
const willLeave = useCallback(
|
||||||
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
|
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
|
||||||
[direction],
|
[direction]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (reduceMotion) {
|
if (reduceMotion) {
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tag: {
|
tag: {
|
||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
history?: {
|
history?: Array<{
|
||||||
uses: number;
|
uses: number;
|
||||||
accounts: string;
|
accounts: string;
|
||||||
day: string;
|
day: string;
|
||||||
}[];
|
}>;
|
||||||
following?: boolean;
|
following?: boolean;
|
||||||
type: 'hashtag';
|
type: 'hashtag';
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { Account } from '../../types/resources';
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account;
|
||||||
size: number;
|
size: number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
|
|
|
@ -3,8 +3,8 @@ import type { Account } from '../../types/resources';
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account;
|
||||||
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
friend: Account;
|
||||||
size?: number;
|
size?: number;
|
||||||
baseSize?: number;
|
baseSize?: number;
|
||||||
overlaySize?: number;
|
overlaySize?: number;
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
// @ts-check
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns custom renderer for one of the common counter types
|
||||||
|
* @param {"statuses" | "following" | "followers"} counterType
|
||||||
|
* Type of the counter
|
||||||
|
* @param {boolean} isBold Whether display number must be displayed in bold
|
||||||
|
* @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||||
|
* Renderer function
|
||||||
|
* @throws If counterType is not covered by this function
|
||||||
|
*/
|
||||||
|
export function counterRenderer(counterType, isBold = true) {
|
||||||
|
/**
|
||||||
|
* @type {(displayNumber: JSX.Element) => JSX.Element}
|
||||||
|
*/
|
||||||
|
const renderCounter = isBold
|
||||||
|
? (displayNumber) => <strong>{displayNumber}</strong>
|
||||||
|
: (displayNumber) => displayNumber;
|
||||||
|
|
||||||
|
switch (counterType) {
|
||||||
|
case 'statuses': {
|
||||||
|
return (displayNumber, pluralReady) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.statuses_counter'
|
||||||
|
defaultMessage='{count, plural, one {{counter} Post} other {{counter} Posts}}'
|
||||||
|
values={{
|
||||||
|
count: pluralReady,
|
||||||
|
counter: renderCounter(displayNumber),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'following': {
|
||||||
|
return (displayNumber, pluralReady) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.following_counter'
|
||||||
|
defaultMessage='{count, plural, one {{counter} Following} other {{counter} Following}}'
|
||||||
|
values={{
|
||||||
|
count: pluralReady,
|
||||||
|
counter: renderCounter(displayNumber),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'followers': {
|
||||||
|
return (displayNumber, pluralReady) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.followers_counter'
|
||||||
|
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
|
||||||
|
values={{
|
||||||
|
count: pluralReady,
|
||||||
|
counter: renderCounter(displayNumber),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,45 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
export const StatusesCounter = (
|
|
||||||
displayNumber: React.ReactNode,
|
|
||||||
pluralReady: number,
|
|
||||||
) => (
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.statuses_counter'
|
|
||||||
defaultMessage='{count, plural, one {{counter} Post} other {{counter} Posts}}'
|
|
||||||
values={{
|
|
||||||
count: pluralReady,
|
|
||||||
counter: <strong>{displayNumber}</strong>,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const FollowingCounter = (
|
|
||||||
displayNumber: React.ReactNode,
|
|
||||||
pluralReady: number,
|
|
||||||
) => (
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.following_counter'
|
|
||||||
defaultMessage='{count, plural, one {{counter} Following} other {{counter} Following}}'
|
|
||||||
values={{
|
|
||||||
count: pluralReady,
|
|
||||||
counter: <strong>{displayNumber}</strong>,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const FollowersCounter = (
|
|
||||||
displayNumber: React.ReactNode,
|
|
||||||
pluralReady: number,
|
|
||||||
) => (
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.followers_counter'
|
|
||||||
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
|
|
||||||
values={{
|
|
||||||
count: pluralReady,
|
|
||||||
counter: <strong>{displayNumber}</strong>,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { bannerSettings } from 'mastodon/settings';
|
||||||
|
|
||||||
|
import { IconButton } from './icon_button';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
|
||||||
|
});
|
||||||
|
|
||||||
|
class DismissableBanner extends PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
visible: !bannerSettings.get(this.props.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDismiss = () => {
|
||||||
|
const { id } = this.props;
|
||||||
|
this.setState({ visible: false }, () => bannerSettings.set(id, true));
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { visible } = this.state;
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { children, intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='dismissable-banner'>
|
||||||
|
<div className='dismissable-banner__message'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='dismissable-banner__action'>
|
||||||
|
<IconButton icon='times' title={intl.formatMessage(messages.dismiss)} onClick={this.handleDismiss} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(DismissableBanner);
|
|
@ -1,47 +0,0 @@
|
||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { bannerSettings } from 'mastodon/settings';
|
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|
||||||
id,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [visible, setVisible] = useState(!bannerSettings.get(id));
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const handleDismiss = useCallback(() => {
|
|
||||||
setVisible(false);
|
|
||||||
bannerSettings.set(id, true);
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
if (!visible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='dismissable-banner'>
|
|
||||||
<div className='dismissable-banner__message'>{children}</div>
|
|
||||||
|
|
||||||
<div className='dismissable-banner__action'>
|
|
||||||
<IconButton
|
|
||||||
icon='times'
|
|
||||||
title={intl.formatMessage(messages.dismiss)}
|
|
||||||
onClick={handleDismiss}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -78,7 +78,7 @@ export class DisplayName extends React.PureComponent<Props> {
|
||||||
} else if (account) {
|
} else if (account) {
|
||||||
let acct = account.get('acct');
|
let acct = account.get('acct');
|
||||||
|
|
||||||
if (!acct.includes('@') && localDomain) {
|
if (acct.indexOf('@') === -1 && localDomain) {
|
||||||
acct = `${acct}@${localDomain}`;
|
acct = `${acct}@${localDomain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const GIFV: React.FC<Props> = ({
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onClick],
|
[onClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -11,7 +11,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
|
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
class SilentErrorBoundary extends Component {
|
class SilentErrorBoundary extends Component {
|
||||||
|
|
|
@ -321,10 +321,7 @@ class MediaGallery extends PureComponent {
|
||||||
if (uncached) {
|
if (uncached) {
|
||||||
spoilerButton = (
|
spoilerButton = (
|
||||||
<button type='button' disabled className='spoiler-button__overlay'>
|
<button type='button' disabled className='spoiler-button__overlay'>
|
||||||
<span className='spoiler-button__overlay__label'>
|
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.uncached_media_warning' defaultMessage='Not available' /></span>
|
||||||
<FormattedMessage id='status.uncached_media_warning' defaultMessage='Preview not available' />
|
|
||||||
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.open' defaultMessage='Click to open' /></span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
} else if (visible) {
|
} else if (visible) {
|
||||||
|
@ -332,10 +329,7 @@ class MediaGallery extends PureComponent {
|
||||||
} else {
|
} else {
|
||||||
spoilerButton = (
|
spoilerButton = (
|
||||||
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
||||||
<span className='spoiler-button__overlay__label'>
|
<span className='spoiler-button__overlay__label'>{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}</span>
|
||||||
{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}
|
|
||||||
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ export const timeAgoString = (
|
||||||
now: number,
|
now: number,
|
||||||
year: number,
|
year: number,
|
||||||
timeGiven: boolean,
|
timeGiven: boolean,
|
||||||
short?: boolean,
|
short?: boolean
|
||||||
) => {
|
) => {
|
||||||
const delta = now - date.getTime();
|
const delta = now - date.getTime();
|
||||||
|
|
||||||
|
@ -118,28 +118,28 @@ export const timeAgoString = (
|
||||||
relativeTime = intl.formatMessage(messages.today);
|
relativeTime = intl.formatMessage(messages.today);
|
||||||
} else if (delta < 10 * SECOND) {
|
} else if (delta < 10 * SECOND) {
|
||||||
relativeTime = intl.formatMessage(
|
relativeTime = intl.formatMessage(
|
||||||
short ? messages.just_now : messages.just_now_full,
|
short ? messages.just_now : messages.just_now_full
|
||||||
);
|
);
|
||||||
} else if (delta < 7 * DAY) {
|
} else if (delta < 7 * DAY) {
|
||||||
if (delta < MINUTE) {
|
if (delta < MINUTE) {
|
||||||
relativeTime = intl.formatMessage(
|
relativeTime = intl.formatMessage(
|
||||||
short ? messages.seconds : messages.seconds_full,
|
short ? messages.seconds : messages.seconds_full,
|
||||||
{ number: Math.floor(delta / SECOND) },
|
{ number: Math.floor(delta / SECOND) }
|
||||||
);
|
);
|
||||||
} else if (delta < HOUR) {
|
} else if (delta < HOUR) {
|
||||||
relativeTime = intl.formatMessage(
|
relativeTime = intl.formatMessage(
|
||||||
short ? messages.minutes : messages.minutes_full,
|
short ? messages.minutes : messages.minutes_full,
|
||||||
{ number: Math.floor(delta / MINUTE) },
|
{ number: Math.floor(delta / MINUTE) }
|
||||||
);
|
);
|
||||||
} else if (delta < DAY) {
|
} else if (delta < DAY) {
|
||||||
relativeTime = intl.formatMessage(
|
relativeTime = intl.formatMessage(
|
||||||
short ? messages.hours : messages.hours_full,
|
short ? messages.hours : messages.hours_full,
|
||||||
{ number: Math.floor(delta / HOUR) },
|
{ number: Math.floor(delta / HOUR) }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
relativeTime = intl.formatMessage(
|
relativeTime = intl.formatMessage(
|
||||||
short ? messages.days : messages.days_full,
|
short ? messages.days : messages.days_full,
|
||||||
{ number: Math.floor(delta / DAY) },
|
{ number: Math.floor(delta / DAY) }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (date.getFullYear() === year) {
|
} else if (date.getFullYear() === year) {
|
||||||
|
@ -158,7 +158,7 @@ const timeRemainingString = (
|
||||||
intl: IntlShape,
|
intl: IntlShape,
|
||||||
date: Date,
|
date: Date,
|
||||||
now: number,
|
now: number,
|
||||||
timeGiven = true,
|
timeGiven = true
|
||||||
) => {
|
) => {
|
||||||
const delta = date.getTime() - now;
|
const delta = date.getTime() - now;
|
||||||
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { History } from 'history';
|
|
||||||
import { createBrowserHistory } from 'history';
|
|
||||||
import { Router as OriginalRouter } from 'react-router';
|
|
||||||
|
|
||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
|
||||||
|
|
||||||
const browserHistory = createBrowserHistory();
|
|
||||||
const originalPush = browserHistory.push.bind(browserHistory);
|
|
||||||
|
|
||||||
browserHistory.push = (path: string, state: History.LocationState) => {
|
|
||||||
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
|
||||||
originalPush(`/deck${path}`, state);
|
|
||||||
} else {
|
|
||||||
originalPush(path, state);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
|
|
||||||
return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
|
|
||||||
};
|
|
|
@ -9,7 +9,7 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchServer } from 'mastodon/actions/server';
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
import Account from 'mastodon/containers/account_container';
|
import Account from 'mastodon/containers/account_container';
|
||||||
import { domain } from 'mastodon/initial_state';
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
|
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback ShortNumberRenderer
|
||||||
|
* @param {JSX.Element} displayNumber Number to display
|
||||||
|
* @param {number} pluralReady Number used for pluralization
|
||||||
|
* @returns {JSX.Element} Final render of number
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ShortNumberProps
|
||||||
|
* @property {number} value Number to display in short variant
|
||||||
|
* @property {ShortNumberRenderer} [renderer]
|
||||||
|
* Custom renderer for numbers, provided as a prop. If another renderer
|
||||||
|
* passed as a child of this component, this prop won't be used.
|
||||||
|
* @property {ShortNumberRenderer} [children]
|
||||||
|
* Custom renderer for numbers, provided as a child. If another renderer
|
||||||
|
* passed as a prop of this component, this one will be used instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders short big number to a shorter version
|
||||||
|
* @param {ShortNumberProps} param0 Props for the component
|
||||||
|
* @returns {JSX.Element} Rendered number
|
||||||
|
*/
|
||||||
|
function ShortNumber({ value, renderer, children }) {
|
||||||
|
const shortNumber = toShortNumber(value);
|
||||||
|
const [, division] = shortNumber;
|
||||||
|
|
||||||
|
if (children != null && renderer != null) {
|
||||||
|
console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRenderer = children != null ? children : renderer;
|
||||||
|
|
||||||
|
const displayNumber = <ShortNumberCounter value={shortNumber} />;
|
||||||
|
|
||||||
|
return customRenderer != null
|
||||||
|
? customRenderer(displayNumber, pluralReady(value, division))
|
||||||
|
: displayNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortNumber.propTypes = {
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
renderer: PropTypes.func,
|
||||||
|
children: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ShortNumberCounterProps
|
||||||
|
* @property {import('../utils/number').ShortNumber} value Short number
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders short number into corresponding localizable react fragment
|
||||||
|
* @param {ShortNumberCounterProps} param0 Props for the component
|
||||||
|
* @returns {JSX.Element} FormattedMessage ready to be embedded in code
|
||||||
|
*/
|
||||||
|
function ShortNumberCounter({ value }) {
|
||||||
|
const [rawNumber, unit, maxFractionDigits = 0] = value;
|
||||||
|
|
||||||
|
const count = (
|
||||||
|
<FormattedNumber
|
||||||
|
value={rawNumber}
|
||||||
|
maximumFractionDigits={maxFractionDigits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
let values = { count, rawNumber };
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case DECIMAL_UNITS.THOUSAND: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='units.short.thousand'
|
||||||
|
defaultMessage='{count}K'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case DECIMAL_UNITS.MILLION: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='units.short.million'
|
||||||
|
defaultMessage='{count}M'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case DECIMAL_UNITS.BILLION: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='units.short.billion'
|
||||||
|
defaultMessage='{count}B'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Not sure if we should go farther - @Sasha-Sorokin
|
||||||
|
default: return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortNumberCounter.propTypes = {
|
||||||
|
value: PropTypes.arrayOf(PropTypes.number),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ShortNumber);
|
|
@ -1,90 +0,0 @@
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
|
||||||
|
|
||||||
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
|
|
||||||
|
|
||||||
type ShortNumberRenderer = (
|
|
||||||
displayNumber: JSX.Element,
|
|
||||||
pluralReady: number,
|
|
||||||
) => JSX.Element;
|
|
||||||
|
|
||||||
interface ShortNumberProps {
|
|
||||||
value: number;
|
|
||||||
renderer?: ShortNumberRenderer;
|
|
||||||
children?: ShortNumberRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShortNumberRenderer: React.FC<ShortNumberProps> = ({
|
|
||||||
value,
|
|
||||||
renderer,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const shortNumber = toShortNumber(value);
|
|
||||||
const [, division] = shortNumber;
|
|
||||||
|
|
||||||
if (children && renderer) {
|
|
||||||
console.warn(
|
|
||||||
'Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const customRenderer = children ?? renderer ?? null;
|
|
||||||
|
|
||||||
const displayNumber = <ShortNumberCounter value={shortNumber} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
customRenderer?.(displayNumber, pluralReady(value, division)) ??
|
|
||||||
displayNumber
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const ShortNumber = memo(ShortNumberRenderer);
|
|
||||||
|
|
||||||
interface ShortNumberCounterProps {
|
|
||||||
value: number[];
|
|
||||||
}
|
|
||||||
const ShortNumberCounter: React.FC<ShortNumberCounterProps> = ({ value }) => {
|
|
||||||
const [rawNumber, unit, maxFractionDigits = 0] = value;
|
|
||||||
|
|
||||||
const count = (
|
|
||||||
<FormattedNumber
|
|
||||||
value={rawNumber}
|
|
||||||
maximumFractionDigits={maxFractionDigits}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const values = { count, rawNumber };
|
|
||||||
|
|
||||||
switch (unit) {
|
|
||||||
case DECIMAL_UNITS.THOUSAND: {
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
id='units.short.thousand'
|
|
||||||
defaultMessage='{count}K'
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case DECIMAL_UNITS.MILLION: {
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
id='units.short.million'
|
|
||||||
defaultMessage='{count}M'
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case DECIMAL_UNITS.BILLION: {
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
id='units.short.billion'
|
|
||||||
defaultMessage='{count}B'
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Not sure if we should go farther - @Sasha-Sorokin
|
|
||||||
default:
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -237,6 +237,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
||||||
const { signedIn, permissions } = this.context.identity;
|
const { signedIn, permissions } = this.context.identity;
|
||||||
|
|
||||||
|
const anonymousAccess = !signedIn;
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
|
@ -258,11 +259,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicStatus && (signedIn || !isRemote)) {
|
if (publicStatus) {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signedIn) {
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||||
|
@ -331,7 +331,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let replyIcon;
|
let replyIcon;
|
||||||
let replyTitle;
|
let replyTitle;
|
||||||
|
@ -372,6 +371,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
<div className='status__action-bar__dropdown'>
|
<div className='status__action-bar__dropdown'>
|
||||||
<DropdownMenuContainer
|
<DropdownMenuContainer
|
||||||
scrollKey={scrollKey}
|
scrollKey={scrollKey}
|
||||||
|
disabled={anonymousAccess}
|
||||||
status={status}
|
status={status}
|
||||||
items={menu}
|
items={menu}
|
||||||
icon='ellipsis-h'
|
icon='ellipsis-h'
|
||||||
|
|
|
@ -44,7 +44,7 @@ class TranslateButton extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className='status__content__translate-button' onClick={onClick}>
|
<button className='status__content__read-more-button' onClick={onClick}>
|
||||||
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Route } from 'react-router-dom';
|
import { BrowserRouter, Route } from 'react-router-dom';
|
||||||
|
|
||||||
import { Provider as ReduxProvider } from 'react-redux';
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
||||||
import { hydrateStore } from 'mastodon/actions/store';
|
import { hydrateStore } from 'mastodon/actions/store';
|
||||||
import { connectUserStream } from 'mastodon/actions/streaming';
|
import { connectUserStream } from 'mastodon/actions/streaming';
|
||||||
import ErrorBoundary from 'mastodon/components/error_boundary';
|
import ErrorBoundary from 'mastodon/components/error_boundary';
|
||||||
import { Router } from 'mastodon/components/router';
|
|
||||||
import UI from 'mastodon/features/ui';
|
import UI from 'mastodon/features/ui';
|
||||||
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
||||||
import { IntlProvider } from 'mastodon/locales';
|
import { IntlProvider } from 'mastodon/locales';
|
||||||
|
@ -76,11 +75,11 @@ export default class Mastodon extends PureComponent {
|
||||||
<IntlProvider>
|
<IntlProvider>
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Router>
|
<BrowserRouter>
|
||||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
<Route path='/' component={UI} />
|
<Route path='/' component={UI} />
|
||||||
</ScrollContext>
|
</ScrollContext>
|
||||||
</Router>
|
</BrowserRouter>
|
||||||
|
|
||||||
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -139,7 +139,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
id: status.get('id'),
|
url: status.get('url'),
|
||||||
onError: error => dispatch(showAlertForError(error)),
|
onError: error => dispatch(showAlertForError(error)),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -11,10 +11,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
import Button from 'mastodon/components/button';
|
import Button from 'mastodon/components/button';
|
||||||
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
|
import { counterRenderer } from 'mastodon/components/common_counter';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
import { autoPlayGif, me, domain } from 'mastodon/initial_state';
|
import { autoPlayGif, me, domain } from 'mastodon/initial_state';
|
||||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
|
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
|
||||||
|
@ -264,14 +264,14 @@ class Header extends ImmutablePureComponent {
|
||||||
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
||||||
actionBtn = '';
|
actionBtn = '';
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
} else if (account.getIn(['relationship', 'requested'])) {
|
||||||
actionBtn = <Button className={classNames({ 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
|
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
|
||||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
||||||
|
@ -290,6 +290,7 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
||||||
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('share' in navigator) {
|
if ('share' in navigator) {
|
||||||
|
@ -450,21 +451,21 @@ class Header extends ImmutablePureComponent {
|
||||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||||
<ShortNumber
|
<ShortNumber
|
||||||
value={account.get('statuses_count')}
|
value={account.get('statuses_count')}
|
||||||
renderer={StatusesCounter}
|
renderer={counterRenderer('statuses')}
|
||||||
/>
|
/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
||||||
<ShortNumber
|
<ShortNumber
|
||||||
value={account.get('following_count')}
|
value={account.get('following_count')}
|
||||||
renderer={FollowingCounter}
|
renderer={counterRenderer('following')}
|
||||||
/>
|
/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
||||||
<ShortNumber
|
<ShortNumber
|
||||||
value={account.get('followers_count')}
|
value={account.get('followers_count')}
|
||||||
renderer={FollowersCounter}
|
renderer={counterRenderer('followers')}
|
||||||
/>
|
/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
import { domain } from 'mastodon/initial_state';
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { openModal } from 'mastodon/actions/modal';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
import Button from 'mastodon/components/button';
|
import Button from 'mastodon/components/button';
|
||||||
import { DisplayName } from 'mastodon/components/display_name';
|
import { DisplayName } from 'mastodon/components/display_name';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
|
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
|
||||||
|
@ -160,16 +160,16 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||||
actionBtn = '';
|
actionBtn = '';
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
} else if (account.getIn(['relationship', 'requested'])) {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
|
actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
|
||||||
} else if (account.getIn(['relationship', 'muting'])) {
|
} else if (account.getIn(['relationship', 'muting'])) {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -25,13 +25,12 @@ export type SearchData = [
|
||||||
BaseEmoji['native'],
|
BaseEmoji['native'],
|
||||||
Emoji['short_names'],
|
Emoji['short_names'],
|
||||||
Search,
|
Search,
|
||||||
Emoji['unified'],
|
Emoji['unified']
|
||||||
];
|
];
|
||||||
|
|
||||||
export type ShortCodesToEmojiData = Record<
|
export interface ShortCodesToEmojiData {
|
||||||
ShortCodesToEmojiDataKey,
|
[key: ShortCodesToEmojiDataKey]: [FilenameData, SearchData];
|
||||||
[FilenameData, SearchData]
|
}
|
||||||
>;
|
|
||||||
export type EmojisWithoutShortCodes = FilenameData[];
|
export type EmojisWithoutShortCodes = FilenameData[];
|
||||||
|
|
||||||
export type EmojiCompressed = [
|
export type EmojiCompressed = [
|
||||||
|
@ -39,7 +38,7 @@ export type EmojiCompressed = [
|
||||||
Skins,
|
Skins,
|
||||||
Category[],
|
Category[],
|
||||||
Data['aliases'],
|
Data['aliases'],
|
||||||
EmojisWithoutShortCodes,
|
EmojisWithoutShortCodes
|
||||||
];
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -9,7 +9,7 @@ import emojiCompressed from './emoji_compressed';
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||||
|
|
||||||
type Emojis = {
|
type Emojis = {
|
||||||
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
|
[key in keyof ShortCodesToEmojiData]: {
|
||||||
native: BaseEmoji['native'];
|
native: BaseEmoji['native'];
|
||||||
search: Search;
|
search: Search;
|
||||||
short_names: Emoji['short_names'];
|
short_names: Emoji['short_names'];
|
||||||
|
|
|
@ -5,7 +5,7 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import { Blurhash } from 'mastodon/components/blurhash';
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
import { accountsCountRenderer } from 'mastodon/components/hashtag';
|
import { accountsCountRenderer } from 'mastodon/components/hashtag';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
export default class Story extends PureComponent {
|
export default class Story extends PureComponent {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { connect } from 'react-redux';
|
||||||
import Column from 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import Search from 'mastodon/features/compose/containers/search_container';
|
import Search from 'mastodon/features/compose/containers/search_container';
|
||||||
import { trendsEnabled } from 'mastodon/initial_state';
|
import { showTrends } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import Links from './links';
|
import Links from './links';
|
||||||
import SearchResults from './results';
|
import SearchResults from './results';
|
||||||
|
@ -26,7 +26,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
layout: state.getIn(['meta', 'layout']),
|
layout: state.getIn(['meta', 'layout']),
|
||||||
isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
|
isSearching: state.getIn(['search', 'submitted']) || !showTrends,
|
||||||
});
|
});
|
||||||
|
|
||||||
class Explore extends PureComponent {
|
class Explore extends PureComponent {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchTrendingLinks } from 'mastodon/actions/trends';
|
import { fetchTrendingLinks } from 'mastodon/actions/trends';
|
||||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
|
|
||||||
import Story from './components/story';
|
import Story from './components/story';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { connect } from 'react-redux';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
|
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
|
||||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
import StatusList from 'mastodon/components/status_list';
|
import StatusList from 'mastodon/components/status_list';
|
||||||
import { getStatusList } from 'mastodon/selectors';
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
|
@ -52,7 +52,6 @@ class Statuses extends PureComponent {
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
trackScroll
|
trackScroll
|
||||||
timelineId='explore'
|
|
||||||
statusIds={statusIds}
|
statusIds={statusIds}
|
||||||
scrollKey='explore-statuses'
|
scrollKey='explore-statuses'
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
|
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
|
||||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { addColumn } from 'mastodon/actions/columns';
|
||||||
import { changeSetting } from 'mastodon/actions/settings';
|
import { changeSetting } from 'mastodon/actions/settings';
|
||||||
import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
|
import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
|
||||||
import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
|
import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
|
||||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
import initialState, { domain } from 'mastodon/initial_state';
|
import initialState, { domain } from 'mastodon/initial_state';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
|
|
@ -142,7 +142,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||||
|
|
||||||
{!multiColumn && <div className='flex-spacer' />}
|
{!multiColumn && <div className='flex-spacer' />}
|
||||||
|
|
||||||
<LinkFooter multiColumn />
|
<LinkFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(multiColumn && showTrends) && <TrendsContainer />}
|
{(multiColumn && showTrends) && <TrendsContainer />}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||||
|
|
||||||
|
class ColumnSettings extends PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { settings, onChange } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(ColumnSettings);
|
|
@ -1,66 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call,
|
|
||||||
@typescript-eslint/no-unsafe-return,
|
|
||||||
@typescript-eslint/no-unsafe-assignment,
|
|
||||||
@typescript-eslint/no-unsafe-member-access
|
|
||||||
-- the settings store is not yet typed */
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
|
||||||
|
|
||||||
import { changeSetting } from '../../../actions/settings';
|
|
||||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
|
||||||
|
|
||||||
export const ColumnSettings: React.FC = () => {
|
|
||||||
const settings = useAppSelector((state) => state.settings.get('home'));
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const onChange = useCallback(
|
|
||||||
(key: string, checked: boolean) => {
|
|
||||||
dispatch(changeSetting(['home', ...key], checked));
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<span className='column-settings__section'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='home.column_settings.basic'
|
|
||||||
defaultMessage='Basic'
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
|
||||||
<SettingToggle
|
|
||||||
prefix='home_timeline'
|
|
||||||
settings={settings}
|
|
||||||
settingPath={['shows', 'reblog']}
|
|
||||||
onChange={onChange}
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id='home.column_settings.show_reblogs'
|
|
||||||
defaultMessage='Show boosts'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
|
||||||
<SettingToggle
|
|
||||||
prefix='home_timeline'
|
|
||||||
settings={settings}
|
|
||||||
settingPath={['shows', 'reply']}
|
|
||||||
onChange={onChange}
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id='home.column_settings.show_replies'
|
|
||||||
defaultMessage='Show replies'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import background from 'mastodon/../images/friends-cropped.png';
|
||||||
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
|
|
||||||
|
|
||||||
|
export const ExplorePrompt = () => (
|
||||||
|
<DismissableBanner id='home.explore_prompt'>
|
||||||
|
<img src={background} alt='' className='dismissable-banner__background-image' />
|
||||||
|
|
||||||
|
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
||||||
|
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
|
||||||
|
|
||||||
|
<div className='dismissable-banner__message__actions__wrapper'>
|
||||||
|
<div className='dismissable-banner__message__actions'>
|
||||||
|
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
||||||
|
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DismissableBanner>
|
||||||
|
);
|
|
@ -1,46 +0,0 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import background from 'mastodon/../images/friends-cropped.png';
|
|
||||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
|
||||||
|
|
||||||
export const ExplorePrompt = () => (
|
|
||||||
<DismissableBanner id='home.explore_prompt'>
|
|
||||||
<img
|
|
||||||
src={background}
|
|
||||||
alt=''
|
|
||||||
className='dismissable-banner__background-image'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
<FormattedMessage
|
|
||||||
id='home.explore_prompt.title'
|
|
||||||
defaultMessage='This is your home base within Mastodon.'
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id='home.explore_prompt.body'
|
|
||||||
defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className='dismissable-banner__message__wrapper'>
|
|
||||||
<div className='dismissable-banner__message__actions'>
|
|
||||||
<Link to='/explore' className='button'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='home.actions.go_to_explore'
|
|
||||||
defaultMessage="See what's trending"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
<Link to='/explore/suggestions' className='button button-tertiary'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='home.actions.go_to_suggestions'
|
|
||||||
defaultMessage='Find people to follow'
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DismissableBanner>
|
|
||||||
);
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { changeSetting, saveSettings } from '../../../actions/settings';
|
||||||
|
import ColumnSettings from '../components/column_settings';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
settings: state.getIn(['settings', 'home']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onChange (key, checked) {
|
||||||
|
dispatch(changeSetting(['home', ...key], checked));
|
||||||
|
},
|
||||||
|
|
||||||
|
onSave () {
|
||||||
|
dispatch(saveSettings());
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
|
@ -22,8 +22,8 @@ import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
|
||||||
import { ColumnSettings } from './components/column_settings';
|
|
||||||
import { ExplorePrompt } from './components/explore_prompt';
|
import { ExplorePrompt } from './components/explore_prompt';
|
||||||
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||||
|
@ -191,7 +191,7 @@ class HomeTimeline extends PureComponent {
|
||||||
extraButton={announcementsButton}
|
extraButton={announcementsButton}
|
||||||
appendContent={hasAnnouncements && showAnnouncements && <AnnouncementsContainer />}
|
appendContent={hasAnnouncements && showAnnouncements && <AnnouncementsContainer />}
|
||||||
>
|
>
|
||||||
<ColumnSettings />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
|
|
|
@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
dispatch(changePushNotifications(path.slice(1), checked));
|
dispatch(changePushNotifications(path.slice(1), checked));
|
||||||
} else {
|
} else {
|
||||||
dispatch(showAlert({ message: messages.permissionDenied }));
|
dispatch(showAlert(undefined, messages.permissionDenied));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
} else {
|
} else {
|
||||||
dispatch(showAlert({ message: messages.permissionDenied }));
|
dispatch(showAlert(undefined, messages.permissionDenied));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
import { domain } from 'mastodon/initial_state';
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
|
|
|
@ -1,75 +1,53 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { OrderedSet, List as ImmutableList } from 'immutable';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { shallowEqual } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
import Toggle from 'react-toggle';
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
import { fetchAccount } from 'mastodon/actions/accounts';
|
|
||||||
import Button from 'mastodon/components/button';
|
import Button from 'mastodon/components/button';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
|
placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectRepliedToAccountIds = createSelector(
|
class Comment extends PureComponent {
|
||||||
[
|
|
||||||
(state) => state.get('statuses'),
|
|
||||||
(_, statusIds) => statusIds,
|
|
||||||
],
|
|
||||||
(statusesMap, statusIds) => statusIds.map((statusId) => statusesMap.getIn([statusId, 'in_reply_to_account_id'])),
|
|
||||||
{
|
|
||||||
resultEqualityCheck: shallowEqual,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedDomains, onSubmit, onChangeComment, onToggleDomain }) => {
|
static propTypes = {
|
||||||
const intl = useIntl();
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
comment: PropTypes.string.isRequired,
|
||||||
|
onChangeComment: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
isSubmitting: PropTypes.bool,
|
||||||
|
forward: PropTypes.bool,
|
||||||
|
isRemote: PropTypes.bool,
|
||||||
|
domain: PropTypes.string,
|
||||||
|
onChangeForward: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
handleClick = () => {
|
||||||
const loadedRef = useRef(false);
|
const { onSubmit } = this.props;
|
||||||
|
onSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
const handleClick = useCallback(() => onSubmit(), [onSubmit]);
|
handleChange = e => {
|
||||||
const handleChange = useCallback((e) => onChangeComment(e.target.value), [onChangeComment]);
|
const { onChangeComment } = this.props;
|
||||||
const handleToggleDomain = useCallback(e => onToggleDomain(e.target.value, e.target.checked), [onToggleDomain]);
|
onChangeComment(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e) => {
|
handleKeyDown = e => {
|
||||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||||
handleClick();
|
this.handleClick();
|
||||||
}
|
}
|
||||||
}, [handleClick]);
|
};
|
||||||
|
|
||||||
// Memoize accountIds since we don't want it to trigger `useEffect` on each render
|
handleForwardChange = e => {
|
||||||
const accountIds = useAppSelector((state) => domain ? selectRepliedToAccountIds(state, statusIds) : ImmutableList());
|
const { onChangeForward } = this.props;
|
||||||
|
onChangeForward(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
// While we could memoize `availableDomains`, it is pretty inexpensive to recompute
|
render () {
|
||||||
const accountsMap = useAppSelector((state) => state.get('accounts'));
|
const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props;
|
||||||
const availableDomains = domain ? OrderedSet([domain]).union(accountIds.map((accountId) => accountsMap.getIn([accountId, 'acct'], '').split('@')[1]).filter(domain => !!domain)) : OrderedSet();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (loadedRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedRef.current = true;
|
|
||||||
|
|
||||||
// First, pre-select known domains
|
|
||||||
availableDomains.forEach((domain) => {
|
|
||||||
onToggleDomain(domain, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then, fetch missing replied-to accounts
|
|
||||||
const unknownAccounts = OrderedSet(accountIds.filter(accountId => accountId && !accountsMap.has(accountId)));
|
|
||||||
unknownAccounts.forEach((accountId) => {
|
|
||||||
dispatch(fetchAccount(accountId));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -79,8 +57,8 @@ const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedD
|
||||||
className='report-dialog-modal__textarea'
|
className='report-dialog-modal__textarea'
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={handleChange}
|
onChange={this.handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -88,34 +66,22 @@ const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedD
|
||||||
<>
|
<>
|
||||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
|
<p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
|
||||||
|
|
||||||
{ availableDomains.map((domain) => (
|
<label className='report-dialog-modal__toggle'>
|
||||||
<label className='report-dialog-modal__toggle' key={`toggle-${domain}`}>
|
<Toggle checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
|
||||||
<Toggle checked={selectedDomains.includes(domain)} disabled={isSubmitting} onChange={handleToggleDomain} value={domain} />
|
|
||||||
<FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />
|
<FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />
|
||||||
</label>
|
</label>
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='flex-spacer' />
|
<div className='flex-spacer' />
|
||||||
|
|
||||||
<div className='report-dialog-modal__actions'>
|
<div className='report-dialog-modal__actions'>
|
||||||
<Button onClick={handleClick} disabled={isSubmitting}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
|
<Button onClick={this.handleClick} disabled={isSubmitting}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Comment.propTypes = {
|
export default injectIntl(Comment);
|
||||||
comment: PropTypes.string.isRequired,
|
|
||||||
domain: PropTypes.string,
|
|
||||||
statusIds: ImmutablePropTypes.list.isRequired,
|
|
||||||
isRemote: PropTypes.bool,
|
|
||||||
isSubmitting: PropTypes.bool,
|
|
||||||
selectedDomains: ImmutablePropTypes.set.isRequired,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
onChangeComment: PropTypes.func.isRequired,
|
|
||||||
onToggleDomain: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Comment;
|
|
||||||
|
|
|
@ -195,22 +195,20 @@ class ActionBar extends PureComponent {
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
if (publicStatus && isRemote) {
|
if (publicStatus) {
|
||||||
|
if (isRemote) {
|
||||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
|
|
||||||
if (publicStatus && 'share' in navigator) {
|
if ('share' in navigator) {
|
||||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicStatus && (signedIn || !isRemote)) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
|
||||||
|
|
||||||
if (signedIn) {
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
if (writtenByMe) {
|
if (writtenByMe) {
|
||||||
if (pinnableStatus) {
|
if (pinnableStatus) {
|
||||||
|
@ -265,7 +263,6 @@ class ActionBar extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let replyIcon;
|
let replyIcon;
|
||||||
if (status.get('in_reply_to_id', null) === null) {
|
if (status.get('in_reply_to_id', null) === null) {
|
||||||
|
@ -295,7 +292,7 @@ class ActionBar extends PureComponent {
|
||||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||||
|
|
||||||
<div className='detailed-status__action-bar-dropdown'>
|
<div className='detailed-status__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
|
<DropdownMenuContainer size={18} icon='ellipsis-h' disabled={!signedIn} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -110,7 +110,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
id: status.get('id'),
|
url: status.get('url'),
|
||||||
onError: error => dispatch(showAlertForError(error)),
|
onError: error => dispatch(showAlertForError(error)),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -449,7 +449,7 @@ class Status extends ImmutablePureComponent {
|
||||||
handleEmbed = (status) => {
|
handleEmbed = (status) => {
|
||||||
this.props.dispatch(openModal({
|
this.props.dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: { id: status.get('id') },
|
modalProps: { url: status.get('url') },
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const messages = defineMessages({
|
||||||
class EmbedModal extends ImmutablePureComponent {
|
class EmbedModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
onError: PropTypes.func.isRequired,
|
onError: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
@ -26,11 +26,11 @@ class EmbedModal extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { id } = this.props;
|
const { url } = this.props;
|
||||||
|
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
api().get(`/api/web/embeds/${id}`).then(res => {
|
api().post('/api/web/embed', { url }).then(res => {
|
||||||
this.setState({ loading: false, oembed: res.data });
|
this.setState({ loading: false, oembed: res.data });
|
||||||
|
|
||||||
const iframeDocument = this.iframe.contentWindow.document;
|
const iframeDocument = this.iframe.contentWindow.document;
|
||||||
|
|
|
@ -38,7 +38,6 @@ class LinkFooter extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -54,7 +53,6 @@ class LinkFooter extends PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { signedIn, permissions } = this.context.identity;
|
const { signedIn, permissions } = this.context.identity;
|
||||||
const { multiColumn } = this.props;
|
|
||||||
|
|
||||||
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
|
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
|
||||||
const canProfileDirectory = profileDirectory;
|
const canProfileDirectory = profileDirectory;
|
||||||
|
@ -66,7 +64,7 @@ class LinkFooter extends PureComponent {
|
||||||
<p>
|
<p>
|
||||||
<strong>{domain}</strong>:
|
<strong>{domain}</strong>:
|
||||||
{' '}
|
{' '}
|
||||||
<Link to='/about' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
|
<Link to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
|
||||||
{statusPageUrl && (
|
{statusPageUrl && (
|
||||||
<>
|
<>
|
||||||
{DividingCircle}
|
{DividingCircle}
|
||||||
|
@ -86,7 +84,7 @@ class LinkFooter extends PureComponent {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{DividingCircle}
|
{DividingCircle}
|
||||||
<Link to='/privacy-policy' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
|
<Link to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -7,8 +7,7 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { WordmarkLogo } from 'mastodon/components/logo';
|
import { WordmarkLogo } from 'mastodon/components/logo';
|
||||||
import NavigationPortal from 'mastodon/components/navigation_portal';
|
import NavigationPortal from 'mastodon/components/navigation_portal';
|
||||||
import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
|
import { timelinePreview, showTrends } from 'mastodon/initial_state';
|
||||||
import { transientSingleColumn } from 'mastodon/is_mobile';
|
|
||||||
|
|
||||||
import ColumnLink from './column_link';
|
import ColumnLink from './column_link';
|
||||||
import DisabledAccountBanner from './disabled_account_banner';
|
import DisabledAccountBanner from './disabled_account_banner';
|
||||||
|
@ -30,7 +29,6 @@ const messages = defineMessages({
|
||||||
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
|
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
|
||||||
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
||||||
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class NavigationPanel extends Component {
|
class NavigationPanel extends Component {
|
||||||
|
@ -56,12 +54,6 @@ class NavigationPanel extends Component {
|
||||||
<div className='navigation-panel'>
|
<div className='navigation-panel'>
|
||||||
<div className='navigation-panel__logo'>
|
<div className='navigation-panel__logo'>
|
||||||
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
||||||
|
|
||||||
{transientSingleColumn && (
|
|
||||||
<a href={`/deck${location.pathname}`} className='button button--block'>
|
|
||||||
{intl.formatMessage(messages.advancedInterface)}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -73,7 +65,7 @@ class NavigationPanel extends Component {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{trendsEnabled ? (
|
{showTrends ? (
|
||||||
<ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
|
<ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
|
||||||
) : (
|
) : (
|
||||||
<ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />
|
<ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />
|
||||||
|
|
|
@ -45,26 +45,25 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
state = {
|
state = {
|
||||||
step: 'category',
|
step: 'category',
|
||||||
selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []),
|
selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []),
|
||||||
selectedDomains: OrderedSet(),
|
|
||||||
comment: '',
|
comment: '',
|
||||||
category: null,
|
category: null,
|
||||||
selectedRuleIds: OrderedSet(),
|
selectedRuleIds: OrderedSet(),
|
||||||
|
forward: true,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
isSubmitted: false,
|
isSubmitted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
const { dispatch, accountId } = this.props;
|
const { dispatch, accountId } = this.props;
|
||||||
const { selectedStatusIds, selectedDomains, comment, category, selectedRuleIds } = this.state;
|
const { selectedStatusIds, comment, category, selectedRuleIds, forward } = this.state;
|
||||||
|
|
||||||
this.setState({ isSubmitting: true });
|
this.setState({ isSubmitting: true });
|
||||||
|
|
||||||
dispatch(submitReport({
|
dispatch(submitReport({
|
||||||
account_id: accountId,
|
account_id: accountId,
|
||||||
status_ids: selectedStatusIds.toArray(),
|
status_ids: selectedStatusIds.toArray(),
|
||||||
selected_domains: selectedDomains.toArray(),
|
|
||||||
comment,
|
comment,
|
||||||
forward: selectedDomains.size > 0,
|
forward,
|
||||||
category,
|
category,
|
||||||
rule_ids: selectedRuleIds.toArray(),
|
rule_ids: selectedRuleIds.toArray(),
|
||||||
}, this.handleSuccess, this.handleFail));
|
}, this.handleSuccess, this.handleFail));
|
||||||
|
@ -88,19 +87,13 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDomainToggle = (domain, checked) => {
|
|
||||||
if (checked) {
|
|
||||||
this.setState((state) => ({ selectedDomains: state.selectedDomains.add(domain) }));
|
|
||||||
} else {
|
|
||||||
this.setState((state) => ({ selectedDomains: state.selectedDomains.remove(domain) }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRuleToggle = (ruleId, checked) => {
|
handleRuleToggle = (ruleId, checked) => {
|
||||||
|
const { selectedRuleIds } = this.state;
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
this.setState((state) => ({ selectedRuleIds: state.selectedRuleIds.add(ruleId) }));
|
this.setState({ selectedRuleIds: selectedRuleIds.add(ruleId) });
|
||||||
} else {
|
} else {
|
||||||
this.setState((state) => ({ selectedRuleIds: state.selectedRuleIds.remove(ruleId) }));
|
this.setState({ selectedRuleIds: selectedRuleIds.remove(ruleId) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -112,6 +105,10 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
this.setState({ comment });
|
this.setState({ comment });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleChangeForward = forward => {
|
||||||
|
this.setState({ forward });
|
||||||
|
};
|
||||||
|
|
||||||
handleNextStep = step => {
|
handleNextStep = step => {
|
||||||
this.setState({ step });
|
this.setState({ step });
|
||||||
};
|
};
|
||||||
|
@ -139,8 +136,8 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
step,
|
step,
|
||||||
selectedStatusIds,
|
selectedStatusIds,
|
||||||
selectedRuleIds,
|
selectedRuleIds,
|
||||||
selectedDomains,
|
|
||||||
comment,
|
comment,
|
||||||
|
forward,
|
||||||
category,
|
category,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
isSubmitted,
|
isSubmitted,
|
||||||
|
@ -188,11 +185,10 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
isRemote={isRemote}
|
isRemote={isRemote}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
|
forward={forward}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
onChangeComment={this.handleChangeComment}
|
onChangeComment={this.handleChangeComment}
|
||||||
statusIds={selectedStatusIds}
|
onChangeForward={this.handleChangeForward}
|
||||||
selectedDomains={selectedDomains}
|
|
||||||
onToggleDomain={this.handleDomainToggle}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -7,27 +7,26 @@ import { NotificationStack } from 'react-notification';
|
||||||
import { dismissAlert } from '../../../actions/alerts';
|
import { dismissAlert } from '../../../actions/alerts';
|
||||||
import { getAlerts } from '../../../selectors';
|
import { getAlerts } from '../../../selectors';
|
||||||
|
|
||||||
const formatIfNeeded = (intl, message, values) => {
|
const mapStateToProps = (state, { intl }) => {
|
||||||
if (typeof message === 'object') {
|
const notifications = getAlerts(state);
|
||||||
return intl.formatMessage(message, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
notifications.forEach(notification => ['title', 'message'].forEach(key => {
|
||||||
|
const value = notification[key];
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { notifications };
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state, { intl }) => ({
|
const mapDispatchToProps = (dispatch) => {
|
||||||
notifications: getAlerts(state).map(alert => ({
|
return {
|
||||||
...alert,
|
onDismiss: alert => {
|
||||||
action: formatIfNeeded(intl, alert.action, alert.values),
|
|
||||||
title: formatIfNeeded(intl, alert.title, alert.values),
|
|
||||||
message: formatIfNeeded(intl, alert.message, alert.values),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
onDismiss (alert) {
|
|
||||||
dispatch(dismissAlert(alert));
|
dispatch(dismissAlert(alert));
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { clearHeight } from '../../actions/height_cache';
|
||||||
import { expandNotifications } from '../../actions/notifications';
|
import { expandNotifications } from '../../actions/notifications';
|
||||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding } from '../../initial_state';
|
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import Header from './components/header';
|
import Header from './components/header';
|
||||||
|
@ -126,11 +126,11 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
singleColumn: PropTypes.bool,
|
mobile: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillMount () {
|
UNSAFE_componentWillMount () {
|
||||||
if (this.props.singleColumn) {
|
if (this.props.mobile) {
|
||||||
document.body.classList.toggle('layout-single-column', true);
|
document.body.classList.toggle('layout-single-column', true);
|
||||||
document.body.classList.toggle('layout-multiple-columns', false);
|
document.body.classList.toggle('layout-multiple-columns', false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -144,9 +144,9 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
this.node.handleChildrenContentChange();
|
this.node.handleChildrenContentChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevProps.singleColumn !== this.props.singleColumn) {
|
if (prevProps.mobile !== this.props.mobile) {
|
||||||
document.body.classList.toggle('layout-single-column', this.props.singleColumn);
|
document.body.classList.toggle('layout-single-column', this.props.mobile);
|
||||||
document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn);
|
document.body.classList.toggle('layout-multiple-columns', !this.props.mobile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,34 +157,30 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, singleColumn } = this.props;
|
const { children, mobile } = this.props;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
const pathName = this.props.location.pathname;
|
|
||||||
|
|
||||||
let redirect;
|
let redirect;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
if (singleColumn) {
|
if (mobile) {
|
||||||
redirect = <Redirect from='/' to='/home' exact />;
|
redirect = <Redirect from='/' to='/home' exact />;
|
||||||
} else {
|
} else {
|
||||||
redirect = <Redirect from='/' to='/deck/getting-started' exact />;
|
redirect = <Redirect from='/' to='/getting-started' exact />;
|
||||||
}
|
}
|
||||||
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
||||||
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
||||||
} else if (trendsEnabled && trendsAsLanding) {
|
} else if (showTrends && trendsAsLanding) {
|
||||||
redirect = <Redirect from='/' to='/explore' exact />;
|
redirect = <Redirect from='/' to='/explore' exact />;
|
||||||
} else {
|
} else {
|
||||||
redirect = <Redirect from='/' to='/about' exact />;
|
redirect = <Redirect from='/' to='/about' exact />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
|
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
|
||||||
<WrappedSwitch>
|
<WrappedSwitch>
|
||||||
{redirect}
|
{redirect}
|
||||||
|
|
||||||
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
|
||||||
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : 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} />
|
||||||
<WrappedRoute path='/about' component={About} content={children} />
|
<WrappedRoute path='/about' component={About} content={children} />
|
||||||
|
@ -577,7 +573,7 @@ class UI extends PureComponent {
|
||||||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
|
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<SwitchingColumnsArea location={location} singleColumn={layout === 'mobile' || layout === 'single-column'}>
|
<SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'}>
|
||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
|
|
||||||
|
|
|
@ -11,21 +11,13 @@ import BundleContainer from '../containers/bundle_container';
|
||||||
|
|
||||||
// Small wrapper to pass multiColumn to the route components
|
// Small wrapper to pass multiColumn to the route components
|
||||||
export class WrappedSwitch extends PureComponent {
|
export class WrappedSwitch extends PureComponent {
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { multiColumn, children } = this.props;
|
const { multiColumn, children } = this.props;
|
||||||
const { location } = this.context.router.route;
|
|
||||||
|
|
||||||
const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
|
|
||||||
? {...location, pathname: location.pathname.slice(5)}
|
|
||||||
: location;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch location={decklessLocation}>
|
<Switch>
|
||||||
{Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
|
{Children.map(children, child => cloneElement(child, { multiColumn }))}
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,13 +69,12 @@
|
||||||
* @property {boolean} reduce_motion
|
* @property {boolean} reduce_motion
|
||||||
* @property {string} repository
|
* @property {string} repository
|
||||||
* @property {boolean} search_enabled
|
* @property {boolean} search_enabled
|
||||||
* @property {boolean} trends_enabled
|
|
||||||
* @property {boolean} single_user_mode
|
* @property {boolean} single_user_mode
|
||||||
* @property {string} source_url
|
* @property {string} source_url
|
||||||
* @property {string} streaming_api_base_url
|
* @property {string} streaming_api_base_url
|
||||||
* @property {boolean} timeline_preview
|
* @property {boolean} timeline_preview
|
||||||
* @property {string} title
|
* @property {string} title
|
||||||
* @property {boolean} show_trends
|
* @property {boolean} trends
|
||||||
* @property {boolean} trends_as_landing_page
|
* @property {boolean} trends_as_landing_page
|
||||||
* @property {boolean} unfollow_modal
|
* @property {boolean} unfollow_modal
|
||||||
* @property {boolean} use_blurhash
|
* @property {boolean} use_blurhash
|
||||||
|
@ -94,13 +93,6 @@ const element = document.getElementById('initial-state');
|
||||||
/** @type {InitialState | undefined} */
|
/** @type {InitialState | undefined} */
|
||||||
const initialState = element?.textContent && JSON.parse(element.textContent);
|
const initialState = element?.textContent && JSON.parse(element.textContent);
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? '';
|
|
||||||
/** @type {boolean} */
|
|
||||||
export const hasMultiColumnPath = initialPath === '/'
|
|
||||||
|| initialPath === '/getting-started'
|
|
||||||
|| initialPath.startsWith('/deck');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template {keyof InitialStateMeta} K
|
* @template {keyof InitialStateMeta} K
|
||||||
* @param {K} prop
|
* @param {K} prop
|
||||||
|
@ -129,8 +121,7 @@ export const reduceMotion = getMeta('reduce_motion');
|
||||||
export const registrationsOpen = getMeta('registrations_open');
|
export const registrationsOpen = getMeta('registrations_open');
|
||||||
export const repository = getMeta('repository');
|
export const repository = getMeta('repository');
|
||||||
export const searchEnabled = getMeta('search_enabled');
|
export const searchEnabled = getMeta('search_enabled');
|
||||||
export const trendsEnabled = getMeta('trends_enabled');
|
export const showTrends = getMeta('trends');
|
||||||
export const showTrends = getMeta('show_trends');
|
|
||||||
export const singleUserMode = getMeta('single_user_mode');
|
export const singleUserMode = getMeta('single_user_mode');
|
||||||
export const source_url = getMeta('source_url');
|
export const source_url = getMeta('source_url');
|
||||||
export const timelinePreview = getMeta('timeline_preview');
|
export const timelinePreview = getMeta('timeline_preview');
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
|
||||||
import { forceSingleColumn, hasMultiColumnPath } from './initial_state';
|
import { forceSingleColumn } from './initial_state';
|
||||||
|
|
||||||
const LAYOUT_BREAKPOINT = 630;
|
const LAYOUT_BREAKPOINT = 630;
|
||||||
|
|
||||||
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
||||||
|
|
||||||
export const transientSingleColumn = !forceSingleColumn && !hasMultiColumnPath;
|
|
||||||
|
|
||||||
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
||||||
export const layoutFromWindow = (): LayoutType => {
|
export const layoutFromWindow = (): LayoutType => {
|
||||||
if (isMobile(window.innerWidth)) {
|
if (isMobile(window.innerWidth)) {
|
||||||
return 'mobile';
|
return 'mobile';
|
||||||
} else if (!forceSingleColumn && !transientSingleColumn) {
|
} else if (forceSingleColumn) {
|
||||||
return 'multi-column';
|
|
||||||
} else {
|
|
||||||
return 'single-column';
|
return 'single-column';
|
||||||
|
} else {
|
||||||
|
return 'multi-column';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -135,8 +135,6 @@
|
||||||
"community.column_settings.remote_only": "Remote only",
|
"community.column_settings.remote_only": "Remote only",
|
||||||
"compose.language.change": "Change language",
|
"compose.language.change": "Change language",
|
||||||
"compose.language.search": "Search languages...",
|
"compose.language.search": "Search languages...",
|
||||||
"compose.published.body": "Post published.",
|
|
||||||
"compose.published.open": "Open",
|
|
||||||
"compose_form.direct_message_warning_learn_more": "Learn more",
|
"compose_form.direct_message_warning_learn_more": "Learn more",
|
||||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
|
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
|
||||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
|
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
|
||||||
|
@ -385,7 +383,6 @@
|
||||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||||
"mute_modal.indefinite": "Indefinite",
|
"mute_modal.indefinite": "Indefinite",
|
||||||
"navigation_bar.about": "About",
|
"navigation_bar.about": "About",
|
||||||
"navigation_bar.advanced_interface": "Open in advanced web interface",
|
|
||||||
"navigation_bar.blocks": "Blocked users",
|
"navigation_bar.blocks": "Blocked users",
|
||||||
"navigation_bar.bookmarks": "Bookmarks",
|
"navigation_bar.bookmarks": "Bookmarks",
|
||||||
"navigation_bar.community_timeline": "Local timeline",
|
"navigation_bar.community_timeline": "Local timeline",
|
||||||
|
@ -619,8 +616,6 @@
|
||||||
"status.history.created": "{name} created {date}",
|
"status.history.created": "{name} created {date}",
|
||||||
"status.history.edited": "{name} edited {date}",
|
"status.history.edited": "{name} edited {date}",
|
||||||
"status.load_more": "Load more",
|
"status.load_more": "Load more",
|
||||||
"status.media.open": "Click to open",
|
|
||||||
"status.media.show": "Click to show",
|
|
||||||
"status.media_hidden": "Media hidden",
|
"status.media_hidden": "Media hidden",
|
||||||
"status.mention": "Mention @{name}",
|
"status.mention": "Mention @{name}",
|
||||||
"status.more": "More",
|
"status.more": "More",
|
||||||
|
@ -651,7 +646,7 @@
|
||||||
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}",
|
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}",
|
||||||
"status.translate": "Translate",
|
"status.translate": "Translate",
|
||||||
"status.translated_from_with": "Translated from {lang} using {provider}",
|
"status.translated_from_with": "Translated from {lang} using {provider}",
|
||||||
"status.uncached_media_warning": "Preview not available",
|
"status.uncached_media_warning": "Not available",
|
||||||
"status.unmute_conversation": "Unmute conversation",
|
"status.unmute_conversation": "Unmute conversation",
|
||||||
"status.unpin": "Unpin from profile",
|
"status.unpin": "Unpin from profile",
|
||||||
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
|
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
|
||||||
|
|
|
@ -368,7 +368,6 @@
|
||||||
"mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
|
"mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
|
||||||
"mute_modal.indefinite": "Indéfinie",
|
"mute_modal.indefinite": "Indéfinie",
|
||||||
"navigation_bar.about": "À propos",
|
"navigation_bar.about": "À propos",
|
||||||
"navigation_bar.advanced_interface": "Ouvrir dans l’interface avancée",
|
|
||||||
"navigation_bar.blocks": "Comptes bloqués",
|
"navigation_bar.blocks": "Comptes bloqués",
|
||||||
"navigation_bar.bookmarks": "Marque-pages",
|
"navigation_bar.bookmarks": "Marque-pages",
|
||||||
"navigation_bar.community_timeline": "Fil public local",
|
"navigation_bar.community_timeline": "Fil public local",
|
||||||
|
|
|
@ -3,19 +3,15 @@ export interface LocaleData {
|
||||||
messages: Record<string, string>;
|
messages: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadedLocale: LocaleData | undefined;
|
let loadedLocale: LocaleData;
|
||||||
|
|
||||||
export function setLocale(locale: LocaleData) {
|
export function setLocale(locale: LocaleData) {
|
||||||
loadedLocale = locale;
|
loadedLocale = locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLocale(): LocaleData {
|
export function getLocale() {
|
||||||
if (!loadedLocale) {
|
if (!loadedLocale && process.env.NODE_ENV === 'development') {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
throw new Error('getLocale() called before any locale has been set');
|
throw new Error('getLocale() called before any locale has been set');
|
||||||
} else {
|
|
||||||
return { locale: 'unknown', messages: {} };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadedLocale;
|
return loadedLocale;
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { isLocaleLoaded, setLocale } from './global_locale';
|
||||||
const localeLoadingSemaphore = new Semaphore(1);
|
const localeLoadingSemaphore = new Semaphore(1);
|
||||||
|
|
||||||
export async function loadLocale() {
|
export async function loadLocale() {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
|
|
||||||
const locale = document.querySelector<HTMLElement>('html')?.lang || 'en';
|
const locale = document.querySelector<HTMLElement>('html')?.lang || 'en';
|
||||||
|
|
||||||
// We use a Semaphore here so only one thing can try to load the locales at
|
// We use a Semaphore here so only one thing can try to load the locales at
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'core-js/features/symbol';
|
||||||
import 'core-js/features/promise/finally';
|
import 'core-js/features/promise/finally';
|
||||||
import { decode as decodeBase64 } from '../utils/base64';
|
import { decode as decodeBase64 } from '../utils/base64';
|
||||||
|
|
||||||
if (!Object.hasOwn(HTMLCanvasElement.prototype, 'toBlob')) {
|
if (!HTMLCanvasElement.prototype.toBlob) {
|
||||||
const BASE64_MARKER = ';base64,';
|
const BASE64_MARKER = ';base64,';
|
||||||
|
|
||||||
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
|
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
|
||||||
|
@ -12,12 +12,12 @@ if (!Object.hasOwn(HTMLCanvasElement.prototype, 'toBlob')) {
|
||||||
this: HTMLCanvasElement,
|
this: HTMLCanvasElement,
|
||||||
callback: BlobCallback,
|
callback: BlobCallback,
|
||||||
type = 'image/png',
|
type = 'image/png',
|
||||||
quality: unknown,
|
quality: unknown
|
||||||
) {
|
) {
|
||||||
const dataURL: string = this.toDataURL(type, quality);
|
const dataURL: string = this.toDataURL(type, quality);
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
if (dataURL.includes(BASE64_MARKER)) {
|
if (dataURL.indexOf(BASE64_MARKER) >= 0) {
|
||||||
const [, base64] = dataURL.split(BASE64_MARKER);
|
const [, base64] = dataURL.split(BASE64_MARKER);
|
||||||
data = decodeBase64(base64);
|
data = decodeBase64(base64);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -24,7 +24,6 @@ export function loadPolyfills() {
|
||||||
// Latest version of Firefox and Safari do not have IntersectionObserver.
|
// Latest version of Firefox and Safari do not have IntersectionObserver.
|
||||||
// Edge does not have requestIdleCallback.
|
// Edge does not have requestIdleCallback.
|
||||||
// This avoids shipping them all the polyfills.
|
// This avoids shipping them all the polyfills.
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types */
|
|
||||||
const needsExtraPolyfills = !(
|
const needsExtraPolyfills = !(
|
||||||
window.AbortController &&
|
window.AbortController &&
|
||||||
window.IntersectionObserver &&
|
window.IntersectionObserver &&
|
||||||
|
@ -32,7 +31,6 @@ export function loadPolyfills() {
|
||||||
'isIntersecting' in IntersectionObserverEntry.prototype &&
|
'isIntersecting' in IntersectionObserverEntry.prototype &&
|
||||||
window.requestIdleCallback
|
window.requestIdleCallback
|
||||||
);
|
);
|
||||||
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
loadIntlPolyfills(),
|
loadIntlPolyfills(),
|
||||||
|
|
|
@ -80,7 +80,6 @@ async function loadIntlPluralRulesPolyfills(locale: string) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
export async function loadIntlPolyfills() {
|
export async function loadIntlPolyfills() {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
|
|
||||||
const locale = document.querySelector('html')?.lang || 'en';
|
const locale = document.querySelector('html')?.lang || 'en';
|
||||||
|
|
||||||
// order is important here
|
// order is important here
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ALERT_SHOW,
|
ALERT_SHOW,
|
||||||
|
@ -8,20 +8,17 @@ import {
|
||||||
|
|
||||||
const initialState = ImmutableList([]);
|
const initialState = ImmutableList([]);
|
||||||
|
|
||||||
let id = 0;
|
|
||||||
|
|
||||||
const addAlert = (state, alert) =>
|
|
||||||
state.push({
|
|
||||||
key: id++,
|
|
||||||
...alert,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function alerts(state = initialState, action) {
|
export default function alerts(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case ALERT_SHOW:
|
case ALERT_SHOW:
|
||||||
return addAlert(state, action.alert);
|
return state.push(ImmutableMap({
|
||||||
|
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||||
|
title: action.title,
|
||||||
|
message: action.message,
|
||||||
|
message_values: action.message_values,
|
||||||
|
}));
|
||||||
case ALERT_DISMISS:
|
case ALERT_DISMISS:
|
||||||
return state.filterNot(item => item.key === action.alert.key);
|
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||||
case ALERT_CLEAR:
|
case ALERT_CLEAR:
|
||||||
return state.clear();
|
return state.clear();
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -26,6 +26,7 @@ import lists from './lists';
|
||||||
import markers from './markers';
|
import markers from './markers';
|
||||||
import media_attachments from './media_attachments';
|
import media_attachments from './media_attachments';
|
||||||
import meta from './meta';
|
import meta from './meta';
|
||||||
|
import { missedUpdatesReducer } from './missed_updates';
|
||||||
import { modalReducer } from './modal';
|
import { modalReducer } from './modal';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
|
@ -81,6 +82,7 @@ const reducers = {
|
||||||
suggestions,
|
suggestions,
|
||||||
polls,
|
polls,
|
||||||
trends,
|
trends,
|
||||||
|
missed_updates: missedUpdatesReducer,
|
||||||
markers,
|
markers,
|
||||||
picture_in_picture,
|
picture_in_picture,
|
||||||
history,
|
history,
|
||||||
|
@ -99,7 +101,7 @@ const initialRootState = Object.fromEntries(
|
||||||
reducer(undefined, {
|
reducer(undefined, {
|
||||||
// empty action
|
// empty action
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
const RootStateRecord = ImmutableRecord(initialRootState, 'RootState');
|
const RootStateRecord = ImmutableRecord(initialRootState, 'RootState');
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
import type { Action } from 'redux';
|
||||||
|
|
||||||
|
import { focusApp, unfocusApp } from '../actions/app';
|
||||||
|
import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
|
||||||
|
|
||||||
|
interface MissedUpdatesState {
|
||||||
|
focused: boolean;
|
||||||
|
unread: number;
|
||||||
|
}
|
||||||
|
const initialState = Record<MissedUpdatesState>({
|
||||||
|
focused: true,
|
||||||
|
unread: 0,
|
||||||
|
})();
|
||||||
|
|
||||||
|
export function missedUpdatesReducer(
|
||||||
|
state = initialState,
|
||||||
|
action: Action<string>
|
||||||
|
) {
|
||||||
|
switch (action.type) {
|
||||||
|
case focusApp.type:
|
||||||
|
return state.set('focused', true).set('unread', 0);
|
||||||
|
case unfocusApp.type:
|
||||||
|
return state.set('focused', false);
|
||||||
|
case NOTIFICATIONS_UPDATE:
|
||||||
|
return state.get('focused')
|
||||||
|
? state
|
||||||
|
: state.update('unread', (x) => x + 1);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ interface PopModalOption {
|
||||||
}
|
}
|
||||||
const popModal = (
|
const popModal = (
|
||||||
state: State,
|
state: State,
|
||||||
{ modalType, ignoreFocus }: PopModalOption,
|
{ modalType, ignoreFocus }: PopModalOption
|
||||||
): State => {
|
): State => {
|
||||||
if (
|
if (
|
||||||
modalType === undefined ||
|
modalType === undefined ||
|
||||||
|
@ -52,12 +52,12 @@ const popModal = (
|
||||||
const pushModal = (
|
const pushModal = (
|
||||||
state: State,
|
state: State,
|
||||||
modalType: ModalType,
|
modalType: ModalType,
|
||||||
modalProps: ModalProps,
|
modalProps: ModalProps
|
||||||
): State => {
|
): State => {
|
||||||
return state.withMutations((record) => {
|
return state.withMutations((record) => {
|
||||||
record.set('ignoreFocus', false);
|
record.set('ignoreFocus', false);
|
||||||
record.update('stack', (stack) =>
|
record.update('stack', (stack) =>
|
||||||
stack.unshift(Modal({ modalType, modalProps })),
|
stack.unshift(Modal({ modalType, modalProps }))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -68,14 +68,14 @@ export function modalReducer(
|
||||||
modalType: ModalType;
|
modalType: ModalType;
|
||||||
ignoreFocus: boolean;
|
ignoreFocus: boolean;
|
||||||
modalProps: Record<string, unknown>;
|
modalProps: Record<string, unknown>;
|
||||||
}>,
|
}>
|
||||||
) {
|
) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case openModal.type:
|
case openModal.type:
|
||||||
return pushModal(
|
return pushModal(
|
||||||
state,
|
state,
|
||||||
action.payload.modalType,
|
action.payload.modalType,
|
||||||
action.payload.modalProps,
|
action.payload.modalProps
|
||||||
);
|
);
|
||||||
case closeModal.type:
|
case closeModal.type:
|
||||||
return popModal(state, action.payload);
|
return popModal(state, action.payload);
|
||||||
|
@ -85,8 +85,8 @@ export function modalReducer(
|
||||||
return state.update('stack', (stack) =>
|
return state.update('stack', (stack) =>
|
||||||
stack.filterNot(
|
stack.filterNot(
|
||||||
// @ts-expect-error TIMELINE_DELETE action is not typed yet.
|
// @ts-expect-error TIMELINE_DELETE action is not typed yet.
|
||||||
(modal) => modal.get('modalProps').statusId === action.id,
|
(modal) => modal.get('modalProps').statusId === action.id
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -3,12 +3,12 @@ const easingOutQuint = (
|
||||||
t: number,
|
t: number,
|
||||||
b: number,
|
b: number,
|
||||||
c: number,
|
c: number,
|
||||||
d: number,
|
d: number
|
||||||
) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
|
) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
|
||||||
const scroll = (
|
const scroll = (
|
||||||
node: Element,
|
node: Element,
|
||||||
key: 'scrollTop' | 'scrollLeft',
|
key: 'scrollTop' | 'scrollLeft',
|
||||||
target: number,
|
target: number
|
||||||
) => {
|
) => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const offset = node[key];
|
const offset = node[key];
|
||||||
|
@ -38,13 +38,11 @@ const scroll = (
|
||||||
const isScrollBehaviorSupported =
|
const isScrollBehaviorSupported =
|
||||||
'scrollBehavior' in document.documentElement.style;
|
'scrollBehavior' in document.documentElement.style;
|
||||||
|
|
||||||
export const scrollRight = (node: Element, position: number) => {
|
export const scrollRight = (node: Element, position: number) =>
|
||||||
if (isScrollBehaviorSupported)
|
isScrollBehaviorSupported
|
||||||
node.scrollTo({ left: position, behavior: 'smooth' });
|
? node.scrollTo({ left: position, behavior: 'smooth' })
|
||||||
else scroll(node, 'scrollLeft', position);
|
: scroll(node, 'scrollLeft', position);
|
||||||
};
|
export const scrollTop = (node: Element) =>
|
||||||
|
isScrollBehaviorSupported
|
||||||
export const scrollTop = (node: Element) => {
|
? node.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
if (isScrollBehaviorSupported) node.scrollTo({ top: 0, behavior: 'smooth' });
|
: scroll(node, 'scrollTop', 0);
|
||||||
else scroll(node, 'scrollTop', 0);
|
|
||||||
};
|
|
||||||
|
|
|
@ -84,16 +84,26 @@ export const makeGetPictureInPicture = () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const ALERT_DEFAULTS = {
|
const getAlertsBase = state => state.get('alerts');
|
||||||
dismissAfter: 5000,
|
|
||||||
style: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
|
export const getAlerts = createSelector([getAlertsBase], (base) => {
|
||||||
alerts.map(item => ({
|
let arr = [];
|
||||||
...ALERT_DEFAULTS,
|
|
||||||
...item,
|
base.forEach(item => {
|
||||||
})).toArray());
|
arr.push({
|
||||||
|
message: item.get('message'),
|
||||||
|
message_values: item.get('message_values'),
|
||||||
|
title: item.get('title'),
|
||||||
|
key: item.get('key'),
|
||||||
|
dismissAfter: 5000,
|
||||||
|
barStyle: {
|
||||||
|
zIndex: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
});
|
||||||
|
|
||||||
export const makeGetNotification = () => createSelector([
|
export const makeGetNotification = () => createSelector([
|
||||||
(_, base) => base,
|
(_, base) => base,
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const store = configureStore({
|
||||||
.concat(
|
.concat(
|
||||||
loadingBarMiddleware({
|
loadingBarMiddleware({
|
||||||
promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
|
promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.concat(errorsMiddleware)
|
.concat(errorsMiddleware)
|
||||||
.concat(soundsMiddleware()),
|
.concat(soundsMiddleware()),
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue