mirror of https://github.com/mastodon/mastodon
Merge branch 'main' into about-mastodon
# Conflicts: # app/javascript/mastodon/features/ui/components/link_footer.jsx
This commit is contained in:
commit
03a435656c
|
@ -236,7 +236,7 @@ module.exports = {
|
|||
},
|
||||
// Common React utilities
|
||||
{
|
||||
pattern: '{classnames,react-helmet,react-router-dom}',
|
||||
pattern: '{classnames,react-helmet,react-router,react-router-dom}',
|
||||
group: 'external',
|
||||
position: 'before',
|
||||
},
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
patreon: mastodon
|
||||
open_collective: mastodon
|
||||
custom: https://sponsor.joinmastodon.org
|
|
@ -0,0 +1,19 @@
|
|||
name: 'Setup Javascript'
|
||||
description: 'Setup a Javascript environment ready to run the Mastodon code'
|
||||
inputs:
|
||||
onlyProduction:
|
||||
description: Only install production dependencies
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
shell: bash
|
||||
run: yarn --frozen-lockfile ${{ inputs.onlyProduction != 'false' && '--production' || '' }}
|
|
@ -0,0 +1,23 @@
|
|||
name: 'Setup RUby'
|
||||
description: 'Setup a Ruby environment ready to run the Mastodon code'
|
||||
inputs:
|
||||
ruby-version:
|
||||
description: The Ruby version to install
|
||||
default: '.ruby-version'
|
||||
additional-system-dependencies:
|
||||
description: 'Additional packages to install'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev ${{ inputs.additional-system-dependencies }}
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ inputs.ruby-version }}
|
||||
bundler-cache: true
|
|
@ -3,7 +3,6 @@
|
|||
extends: [
|
||||
'config:recommended',
|
||||
':labels(dependencies)',
|
||||
':maintainLockFilesMonthly', // update non-direct dependencies monthly
|
||||
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
|
||||
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
|
||||
],
|
||||
|
|
|
@ -11,6 +11,7 @@ permissions:
|
|||
jobs:
|
||||
compute-suffix:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'mastodon/mastodon'
|
||||
steps:
|
||||
- id: version_vars
|
||||
env:
|
||||
|
|
|
@ -27,14 +27,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Run bundler-audit
|
||||
run: bundle exec bundler-audit
|
||||
|
|
|
@ -19,25 +19,11 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Check for missing strings in English JSON
|
||||
run: |
|
||||
|
|
|
@ -11,6 +11,7 @@ permissions:
|
|||
jobs:
|
||||
download-translations:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'mastodon/mastodon'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -43,14 +44,8 @@ jobs:
|
|||
run: sudo chown -R runner:docker .
|
||||
|
||||
# This is needed to run the normalize step
|
||||
- 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: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Run i18n normalize task
|
||||
run: bundle exec i18n-tasks normalize
|
||||
|
|
|
@ -35,14 +35,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- uses: xt0rted/stylelint-problem-matcher@v1
|
||||
|
||||
|
|
|
@ -30,16 +30,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
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: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Run haml-lint
|
||||
run: |
|
||||
|
|
|
@ -39,14 +39,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: ESLint
|
||||
run: yarn lint:js --max-warnings 0
|
||||
|
|
|
@ -31,14 +31,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Prettier
|
||||
run: yarn lint:json
|
||||
|
|
|
@ -31,14 +31,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Prettier
|
||||
run: yarn lint:md
|
||||
|
|
|
@ -31,14 +31,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Set-up RuboCop Problem Matcher
|
||||
uses: r7kamura/rubocop-problem-matchers-action@v1
|
||||
|
|
|
@ -33,14 +33,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Prettier
|
||||
run: yarn lint:yml
|
||||
|
|
|
@ -35,14 +35,8 @@ jobs:
|
|||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Jest testing
|
||||
run: yarn jest --reporters github-actions summary
|
||||
|
|
|
@ -72,16 +72,8 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev
|
||||
|
||||
- name: Set up bundler cache
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Create database
|
||||
run: './bin/rails db:create'
|
||||
|
|
|
@ -71,16 +71,8 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev
|
||||
|
||||
- name: Set up bundler cache
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Create database
|
||||
run: './bin/rails db:create'
|
||||
|
|
|
@ -34,24 +34,14 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
onlyProduction: 'true'
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev
|
||||
|
||||
- name: Set up bundler cache
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: .ruby-version
|
||||
bundler-cache: true
|
||||
|
||||
- run: yarn --frozen-lockfile --production
|
||||
- name: Precompile assets
|
||||
# Previously had set this, but it's not supported
|
||||
# export NODE_OPTIONS=--openssl-legacy-provider
|
||||
|
@ -113,6 +103,7 @@ jobs:
|
|||
CAS_ENABLED: true
|
||||
BUNDLE_WITH: 'pam_authentication test'
|
||||
CI_JOBS: ${{ matrix.ci_job }}/4
|
||||
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@ -134,20 +125,11 @@ jobs:
|
|||
path: './public'
|
||||
name: ${{ github.sha }}
|
||||
|
||||
- name: Update package index
|
||||
run: sudo apt-get update
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||
|
||||
- name: Install additional system dependencies
|
||||
run: sudo apt-get install -y ffmpeg imagemagick libpam-dev
|
||||
|
||||
- name: Set up bundler cache
|
||||
uses: ruby/setup-ruby@v1
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby-version}}
|
||||
bundler-cache: true
|
||||
additional-system-dependencies: ffmpeg imagemagick libpam-dev
|
||||
|
||||
- name: Load database schema
|
||||
run: './bin/rails db:create db:schema:load db:seed'
|
||||
|
@ -209,28 +191,14 @@ jobs:
|
|||
path: './public'
|
||||
name: ${{ github.sha }}
|
||||
|
||||
- name: Update package index
|
||||
run: sudo apt-get update
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||
|
||||
- name: Install additional system dependencies
|
||||
run: sudo apt-get install -y ffmpeg imagemagick
|
||||
|
||||
- name: Set up bundler cache
|
||||
uses: ruby/setup-ruby@v1
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby-version}}
|
||||
bundler-cache: true
|
||||
additional-system-dependencies: ffmpeg imagemagick
|
||||
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Load database schema
|
||||
run: './bin/rails db:create db:schema:load db:seed'
|
||||
|
@ -327,28 +295,14 @@ jobs:
|
|||
path: './public'
|
||||
name: ${{ github.sha }}
|
||||
|
||||
- name: Update package index
|
||||
run: sudo apt-get update
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||
|
||||
- name: Install additional system dependencies
|
||||
run: sudo apt-get install -y ffmpeg imagemagick
|
||||
|
||||
- name: Set up bundler cache
|
||||
uses: ruby/setup-ruby@v1
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby-version}}
|
||||
bundler-cache: true
|
||||
additional-system-dependencies: ffmpeg imagemagick
|
||||
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Load database schema
|
||||
run: './bin/rails db:create db:schema:load db:seed'
|
||||
|
|
|
@ -1,41 +1,20 @@
|
|||
# This configuration was generated by
|
||||
# `haml-lint --auto-gen-config`
|
||||
# on 2023-10-03 08:32:28 -0400 using Haml-Lint version 0.51.0.
|
||||
# on 2023-10-25 08:29:48 -0400 using Haml-Lint version 0.51.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the lints are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of Haml-Lint, may require this file to be generated again.
|
||||
|
||||
linters:
|
||||
# Offense count: 944
|
||||
# Offense count: 945
|
||||
LineLength:
|
||||
enabled: false
|
||||
|
||||
# Offense count: 22
|
||||
UnnecessaryStringOutput:
|
||||
enabled: false
|
||||
|
||||
# Offense count: 44
|
||||
# Offense count: 10
|
||||
RuboCop:
|
||||
enabled: false
|
||||
|
||||
# Offense count: 3
|
||||
ViewLength:
|
||||
exclude:
|
||||
- 'app/views/admin/accounts/show.html.haml'
|
||||
- 'app/views/admin/reports/show.html.haml'
|
||||
- 'app/views/disputes/strikes/show.html.haml'
|
||||
|
||||
# Offense count: 15
|
||||
InstanceVariables:
|
||||
exclude:
|
||||
- 'app/views/admin/reports/_actions.html.haml'
|
||||
- 'app/views/auth/registrations/_status.html.haml'
|
||||
- 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml'
|
||||
- 'app/views/relationships/_account.html.haml'
|
||||
|
||||
# Offense count: 2
|
||||
IdNames:
|
||||
exclude:
|
||||
- 'app/views/oauth/authorizations/error.html.haml'
|
||||
- 'app/views/shared/_error_messages.html.haml'
|
||||
- 'app/views/admin/accounts/_buttons.html.haml'
|
||||
- 'app/views/admin/accounts/_local_account.html.haml'
|
||||
- 'app/views/admin/roles/_form.html.haml'
|
||||
- 'app/views/layouts/application.html.haml'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.56.1.
|
||||
# using RuboCop version 1.57.1.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
|
@ -48,27 +48,6 @@ Lint/UnusedBlockArgument:
|
|||
- 'config/initializers/paperclip.rb'
|
||||
- 'config/initializers/simple_form.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- 'app/services/activitypub/process_status_update_service.rb'
|
||||
- 'config/initializers/3_omniauth.rb'
|
||||
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
|
||||
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
|
||||
- 'spec/controllers/api/v1/favourites_controller_spec.rb'
|
||||
- 'spec/controllers/concerns/account_controller_concern_spec.rb'
|
||||
- 'spec/helpers/jsonld_helper_spec.rb'
|
||||
- 'spec/models/account_spec.rb'
|
||||
- 'spec/models/domain_block_spec.rb'
|
||||
- 'spec/models/status_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
- 'spec/models/webauthn_credentials_spec.rb'
|
||||
- 'spec/services/account_search_service_spec.rb'
|
||||
- 'spec/services/post_status_service_spec.rb'
|
||||
- 'spec/services/precompute_feed_service_spec.rb'
|
||||
- 'spec/services/resolve_url_service_spec.rb'
|
||||
- 'spec/views/statuses/show.html.haml_spec.rb'
|
||||
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
||||
Metrics/AbcSize:
|
||||
Max: 144
|
||||
|
@ -86,26 +65,6 @@ Metrics/CyclomaticComplexity:
|
|||
Metrics/PerceivedComplexity:
|
||||
Max: 27
|
||||
|
||||
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
||||
# SupportedStyles: snake_case, normalcase, non_integer
|
||||
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
||||
Naming/VariableNumber:
|
||||
Exclude:
|
||||
- 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
|
||||
- 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
|
||||
- 'db/migrate/20190820003045_update_statuses_index.rb'
|
||||
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
|
||||
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
|
||||
- 'spec/models/account_spec.rb'
|
||||
- 'spec/models/domain_block_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: SafeMultiline.
|
||||
Performance/DeletePrefix:
|
||||
Exclude:
|
||||
- 'app/models/featured_tag.rb'
|
||||
|
||||
Performance/MapMethodChain:
|
||||
Exclude:
|
||||
- 'app/models/feed.rb'
|
||||
|
@ -119,7 +78,6 @@ RSpec/AnyInstance:
|
|||
- 'spec/controllers/admin/accounts_controller_spec.rb'
|
||||
- 'spec/controllers/admin/resets_controller_spec.rb'
|
||||
- 'spec/controllers/admin/settings/branding_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/media_controller_spec.rb'
|
||||
- 'spec/controllers/auth/sessions_controller_spec.rb'
|
||||
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
|
||||
- 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'
|
||||
|
@ -178,9 +136,7 @@ RSpec/LetSetup:
|
|||
- 'spec/controllers/admin/reports/actions_controller_spec.rb'
|
||||
- 'spec/controllers/admin/statuses_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/filters_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/followed_tags_controller_spec.rb'
|
||||
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
|
||||
- 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
|
||||
- 'spec/controllers/api/v2/filters/statuses_controller_spec.rb'
|
||||
|
@ -221,7 +177,6 @@ RSpec/LetSetup:
|
|||
|
||||
RSpec/MessageChain:
|
||||
Exclude:
|
||||
- 'spec/controllers/api/v1/media_controller_spec.rb'
|
||||
- 'spec/models/concerns/remotable_spec.rb'
|
||||
- 'spec/models/session_activation_spec.rb'
|
||||
- 'spec/models/setting_spec.rb'
|
||||
|
@ -260,19 +215,6 @@ Rails/ApplicationController:
|
|||
Exclude:
|
||||
- 'app/controllers/health_controller.rb'
|
||||
|
||||
# Configuration parameters: Include.
|
||||
# Include: db/**/*.rb
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Exclude:
|
||||
- 'db/migrate/20170508230434_create_conversation_mutes.rb'
|
||||
- 'db/migrate/20170823162448_create_status_pins.rb'
|
||||
- 'db/migrate/20171116161857_create_list_accounts.rb'
|
||||
- 'db/migrate/20180929222014_create_account_conversations.rb'
|
||||
- 'db/migrate/20181007025445_create_pghero_space_stats.rb'
|
||||
- 'db/migrate/20190103124649_create_scheduled_statuses.rb'
|
||||
- 'db/migrate/20220824233535_create_status_trends.rb'
|
||||
- 'db/migrate/20221006061337_create_preview_card_trends.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: Severity.
|
||||
Rails/DuplicateAssociation:
|
||||
|
@ -314,7 +256,6 @@ Rails/LexicallyScopedActionFilter:
|
|||
Exclude:
|
||||
- 'app/controllers/auth/passwords_controller.rb'
|
||||
- 'app/controllers/auth/registrations_controller.rb'
|
||||
- 'app/controllers/auth/sessions_controller.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Rails/NegateInclude:
|
||||
|
@ -330,7 +271,6 @@ Rails/NegateInclude:
|
|||
- 'app/models/custom_filter.rb'
|
||||
- 'app/services/activitypub/process_status_update_service.rb'
|
||||
- 'app/services/fetch_link_card_service.rb'
|
||||
- 'app/services/search_service.rb'
|
||||
- 'app/workers/web/push_notification_worker.rb'
|
||||
- 'lib/paperclip/color_extractor.rb'
|
||||
|
||||
|
@ -350,24 +290,6 @@ Rails/RakeEnvironment:
|
|||
- 'lib/tasks/repo.rake'
|
||||
- 'lib/tasks/statistics.rake'
|
||||
|
||||
# Configuration parameters: Include.
|
||||
# Include: db/**/*.rb
|
||||
Rails/ReversibleMigration:
|
||||
Exclude:
|
||||
- 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb'
|
||||
- 'db/migrate/20161122163057_remove_unneeded_indexes.rb'
|
||||
- 'db/migrate/20170205175257_remove_devices.rb'
|
||||
- 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb'
|
||||
- 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb'
|
||||
- 'db/migrate/20170609145826_remove_default_language_from_statuses.rb'
|
||||
- 'db/migrate/20170711225116_fix_null_booleans.rb'
|
||||
- 'db/migrate/20171129172043_add_index_on_stream_entries.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/20180617162849_remove_unused_indexes.rb'
|
||||
- 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb'
|
||||
|
||||
# Configuration parameters: ForbiddenMethods, AllowedMethods.
|
||||
# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
|
||||
Rails/SkipsModelValidations:
|
||||
|
@ -416,35 +338,10 @@ Rails/SkipsModelValidations:
|
|||
- 'lib/mastodon/cli/accounts.rb'
|
||||
- 'lib/mastodon/cli/main.rb'
|
||||
- 'lib/mastodon/cli/maintenance.rb'
|
||||
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
|
||||
- 'spec/lib/activitypub/activity/follow_spec.rb'
|
||||
- 'spec/services/follow_service_spec.rb'
|
||||
- 'spec/services/update_account_service_spec.rb'
|
||||
|
||||
# Configuration parameters: Include.
|
||||
# Include: db/**/*.rb
|
||||
Rails/ThreeStateBooleanColumn:
|
||||
Exclude:
|
||||
- 'db/migrate/20160325130944_add_admin_to_users.rb'
|
||||
- 'db/migrate/20161123093447_add_sensitive_to_statuses.rb'
|
||||
- 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb'
|
||||
- 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb'
|
||||
- 'db/migrate/20170209184350_add_reply_to_statuses.rb'
|
||||
- 'db/migrate/20170330163835_create_imports.rb'
|
||||
- 'db/migrate/20170905165803_add_local_to_statuses.rb'
|
||||
- 'db/migrate/20181203021853_add_discoverable_to_accounts.rb'
|
||||
- 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb'
|
||||
- 'db/migrate/20190805123746_add_capabilities_to_tags.rb'
|
||||
- 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb'
|
||||
- 'db/migrate/20200309150742_add_forwarded_to_reports.rb'
|
||||
- 'db/migrate/20210609202149_create_login_activities.rb'
|
||||
- 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb'
|
||||
- 'db/migrate/20211031031021_create_preview_card_providers.rb'
|
||||
- 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb'
|
||||
- 'db/migrate/20220202200743_add_trendable_to_accounts.rb'
|
||||
- 'db/migrate/20220202200926_add_trendable_to_statuses.rb'
|
||||
- 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb'
|
||||
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
|
@ -508,7 +405,7 @@ Style/CaseEquality:
|
|||
Exclude:
|
||||
- 'config/initializers/trusted_proxies.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
# AllowedMethods: ==, equal?, eql?
|
||||
Style/ClassEqualityComparison:
|
||||
|
@ -520,12 +417,6 @@ Style/ClassVars:
|
|||
Exclude:
|
||||
- 'config/initializers/devise.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Style/CombinableLoops:
|
||||
Exclude:
|
||||
- 'app/models/form/custom_emoji_batch.rb'
|
||||
- 'app/models/form/ip_block_batch.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: AllowedVars.
|
||||
Style/FetchEnvVar:
|
||||
|
@ -722,7 +613,6 @@ Style/RedundantReturn:
|
|||
Style/SafeNavigation:
|
||||
Exclude:
|
||||
- 'app/models/concerns/account_finder_concern.rb'
|
||||
- 'app/models/status.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
|
|
2369
CHANGELOG.md
2369
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
|
@ -28,7 +28,7 @@ RUN apt-get update && \
|
|||
libgdbm-dev \
|
||||
libgmp-dev \
|
||||
libssl-dev \
|
||||
libyaml-0-2 \
|
||||
libyaml-dev \
|
||||
ca-certificates \
|
||||
libreadline8 \
|
||||
python3 \
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -4,7 +4,7 @@ source 'https://rubygems.org'
|
|||
ruby '>= 3.0.0'
|
||||
|
||||
gem 'puma', '~> 6.3'
|
||||
gem 'rails', '~> 7.0'
|
||||
gem 'rails', '~> 7.1.1'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 1.2'
|
||||
gem 'rack', '~> 2.2.7'
|
||||
|
@ -106,6 +106,9 @@ group :test do
|
|||
# Used to split testing into chunks in CI
|
||||
gem 'rspec_chunked', '~> 0.6'
|
||||
|
||||
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
|
||||
gem 'rspec-github', '~> 2.4', require: false
|
||||
|
||||
# RSpec progress bar formatter
|
||||
gem 'fuubar', '~> 2.5'
|
||||
|
||||
|
|
222
Gemfile.lock
222
Gemfile.lock
|
@ -39,75 +39,83 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actioncable (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activejob (= 7.1.1)
|
||||
activerecord (= 7.1.1)
|
||||
activestorage (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionmailer (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
actionview (= 7.1.1)
|
||||
activejob (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.1)
|
||||
actionview (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activerecord (= 7.1.1)
|
||||
activestorage (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionview (7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_model_serializers (0.10.14)
|
||||
actionpack (>= 4.1)
|
||||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activejob (7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activerecord (7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activestorage (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activemodel (7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
activerecord (7.1.1)
|
||||
activemodel (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activejob (= 7.1.1)
|
||||
activerecord (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.8)
|
||||
activesupport (7.1.1)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
|
@ -146,18 +154,19 @@ GEM
|
|||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.18)
|
||||
bcrypt (3.1.19)
|
||||
better_errors (2.10.1)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
rouge (>= 1.0.0)
|
||||
better_html (2.0.1)
|
||||
better_html (2.0.2)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.1.4)
|
||||
bindata (2.4.15)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
|
@ -210,17 +219,17 @@ GEM
|
|||
database_cleaner-core (2.0.1)
|
||||
date (3.3.3)
|
||||
debug_inspector (1.1.0)
|
||||
devise (4.9.2)
|
||||
devise (4.9.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (4.1.0)
|
||||
activesupport (< 7.1)
|
||||
devise-two-factor (4.1.1)
|
||||
activesupport (~> 7.0)
|
||||
attr_encrypted (>= 1.3, < 5, != 2)
|
||||
devise (~> 4.0)
|
||||
railties (< 7.1)
|
||||
railties (~> 7.0)
|
||||
rotp (~> 6.0)
|
||||
devise_pam_authenticatable2 (9.2.0)
|
||||
devise (>= 4.0.0)
|
||||
|
@ -237,6 +246,8 @@ GEM
|
|||
dotenv-rails (2.8.1)
|
||||
dotenv (= 2.8.1)
|
||||
railties (>= 3.2)
|
||||
drb (2.1.1)
|
||||
ruby2_keywords
|
||||
ed25519 (1.3.0)
|
||||
elasticsearch (7.13.3)
|
||||
elasticsearch-api (= 7.13.3)
|
||||
|
@ -305,8 +316,8 @@ GEM
|
|||
fuubar (2.5.1)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
haml (6.2.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
|
@ -345,19 +356,23 @@ GEM
|
|||
rainbow (>= 2.0.0)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (1.0.12)
|
||||
i18n-tasks (1.0.13)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
better_html (>= 1.0, < 3.0)
|
||||
erubi
|
||||
highline (>= 2.0.0)
|
||||
i18n
|
||||
parser (>= 2.2.3.0)
|
||||
parser (>= 3.2.2.1)
|
||||
rails-i18n
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
terminal-table (>= 1.5.1)
|
||||
idn-ruby (0.1.5)
|
||||
io-console (0.6.0)
|
||||
ipaddress (0.8.3)
|
||||
irb (1.8.1)
|
||||
rdoc
|
||||
reline (>= 0.3.8)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
json-canonicalization (0.3.2)
|
||||
|
@ -412,12 +427,12 @@ GEM
|
|||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
lograge (0.13.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.21.3)
|
||||
loofah (2.21.4)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
|
@ -434,21 +449,21 @@ GEM
|
|||
azure-storage-blob (~> 2.0.1)
|
||||
hashie (~> 5.0)
|
||||
memory_profiler (1.0.1)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.5.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0808)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
minitest (5.19.0)
|
||||
minitest (5.20.0)
|
||||
msgpack (1.7.1)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.3.0)
|
||||
mutex_m (0.1.2)
|
||||
net-http (0.3.2)
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.3.7)
|
||||
net-imap (0.4.1)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.18.0)
|
||||
|
@ -456,7 +471,7 @@ GEM
|
|||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
timeout
|
||||
net-smtp (0.3.3)
|
||||
net-smtp (0.4.0)
|
||||
net-protocol
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.4)
|
||||
|
@ -493,7 +508,7 @@ GEM
|
|||
orm_adapter (0.5.0)
|
||||
ox (2.14.17)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.3)
|
||||
parser (3.2.2.4)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
parslet (2.0.0)
|
||||
|
@ -512,8 +527,10 @@ GEM
|
|||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
private_address_check (0.5.0)
|
||||
psych (5.1.1)
|
||||
stringio
|
||||
public_suffix (5.0.3)
|
||||
puma (6.3.1)
|
||||
puma (6.4.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -534,63 +551,73 @@ GEM
|
|||
rack
|
||||
rack-proxy (0.7.6)
|
||||
rack
|
||||
rack-session (1.0.1)
|
||||
rack (< 3)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (7.0.8)
|
||||
actioncable (= 7.0.8)
|
||||
actionmailbox (= 7.0.8)
|
||||
actionmailer (= 7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actiontext (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.1)
|
||||
actioncable (= 7.1.1)
|
||||
actionmailbox (= 7.1.1)
|
||||
actionmailer (= 7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
actiontext (= 7.1.1)
|
||||
actionview (= 7.1.1)
|
||||
activejob (= 7.1.1)
|
||||
activemodel (= 7.1.1)
|
||||
activerecord (= 7.1.1)
|
||||
activestorage (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.8)
|
||||
railties (= 7.1.1)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.1.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails-i18n (7.0.7)
|
||||
rails-i18n (7.0.8)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
method_source
|
||||
railties (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rdf (3.2.11)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.6.1)
|
||||
rdf (~> 3.2)
|
||||
rdoc (6.5.0)
|
||||
psych (>= 4.0.0)
|
||||
redcarpet (3.6.0)
|
||||
redis (4.8.1)
|
||||
redis-namespace (1.11.0)
|
||||
redis (>= 4)
|
||||
redlock (1.3.2)
|
||||
redis (>= 3.0.0, < 6.0)
|
||||
regexp_parser (2.8.1)
|
||||
regexp_parser (2.8.2)
|
||||
reline (0.3.9)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.0)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.2.6)
|
||||
rotp (6.2.2)
|
||||
rotp (6.3.0)
|
||||
rouge (4.1.2)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (2.2.0)
|
||||
|
@ -602,6 +629,8 @@ GEM
|
|||
rspec-expectations (3.12.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-github (2.4.0)
|
||||
rspec-core (~> 3.0)
|
||||
rspec-mocks (3.12.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
|
@ -620,12 +649,12 @@ GEM
|
|||
sidekiq (>= 5, < 8)
|
||||
rspec-support (3.12.1)
|
||||
rspec_chunked (0.6)
|
||||
rubocop (1.56.4)
|
||||
rubocop (1.57.1)
|
||||
base64 (~> 0.1.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.2.3)
|
||||
parser (>= 3.2.2.4)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
|
@ -634,11 +663,11 @@ GEM
|
|||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.29.0)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-capybara (2.18.0)
|
||||
rubocop-capybara (2.19.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.23.1)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-performance (1.19.0)
|
||||
rubocop-performance (1.19.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.20.2)
|
||||
|
@ -671,7 +700,7 @@ GEM
|
|||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
semantic_range (3.0.0)
|
||||
sidekiq (6.5.11)
|
||||
sidekiq (6.5.12)
|
||||
connection_pool (>= 2.2.5, < 3)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.5.0, < 5)
|
||||
|
@ -710,6 +739,7 @@ GEM
|
|||
statsd-ruby (1.5.0)
|
||||
stoplight (3.0.2)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.0.8)
|
||||
strong_migrations (0.8.0)
|
||||
activerecord (>= 5.2)
|
||||
swd (1.3.0)
|
||||
|
@ -781,6 +811,7 @@ GEM
|
|||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webrick (1.8.1)
|
||||
websocket (1.2.10)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
|
@ -789,7 +820,7 @@ GEM
|
|||
xorcist (1.1.3)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.11)
|
||||
zeitwerk (2.6.12)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -876,7 +907,7 @@ DEPENDENCIES
|
|||
rack-attack (~> 6.6)
|
||||
rack-cors (~> 2.0)
|
||||
rack-test (~> 2.1)
|
||||
rails (~> 7.0)
|
||||
rails (~> 7.1.1)
|
||||
rails-controller-testing (~> 1.0)
|
||||
rails-i18n (~> 7.0)
|
||||
rails-settings-cached (~> 0.6)!
|
||||
|
@ -885,6 +916,7 @@ DEPENDENCIES
|
|||
redis (~> 4.5)
|
||||
redis-namespace (~> 1.10)
|
||||
rqrcode (~> 2.2)
|
||||
rspec-github (~> 2.4)
|
||||
rspec-rails (~> 6.0)
|
||||
rspec-sidekiq (~> 4.0)
|
||||
rspec_chunked (~> 0.6)
|
||||
|
@ -926,4 +958,4 @@ RUBY VERSION
|
|||
ruby 3.2.2p53
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.13
|
||||
2.4.20
|
||||
|
|
|
@ -70,7 +70,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
|
|||
- **PostgreSQL** 9.5+
|
||||
- **Redis** 4+
|
||||
- **Ruby** 2.7+
|
||||
- **Node.js** 14+
|
||||
- **Node.js** 16+
|
||||
|
||||
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ module Admin
|
|||
private
|
||||
|
||||
def set_instance
|
||||
@instance = Instance.find(TagManager.instance.normalize_domain(params[:id]&.strip))
|
||||
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(params[:id]&.strip))
|
||||
end
|
||||
|
||||
def set_instances
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Apps::CredentialsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read }
|
||||
|
||||
def show
|
||||
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
|
||||
return doorkeeper_render_error unless valid_doorkeeper_token?
|
||||
|
||||
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key client_id scopes)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
|
|||
include DomainControlHelper
|
||||
include DatabaseHelper
|
||||
include AuthorizedFetchHelper
|
||||
include SelfDestructHelper
|
||||
|
||||
helper_method :current_account
|
||||
helper_method :current_session
|
||||
|
@ -39,6 +40,8 @@ class ApplicationController < ActionController::Base
|
|||
service_unavailable
|
||||
end
|
||||
|
||||
before_action :check_self_destruct!
|
||||
|
||||
before_action :store_referrer, except: :raise_not_found, if: :devise_controller?
|
||||
before_action :require_functional!, if: :user_signed_in?
|
||||
|
||||
|
@ -170,6 +173,15 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def check_self_destruct!
|
||||
return unless self_destruct?
|
||||
|
||||
respond_to do |format|
|
||||
format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] }
|
||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code }
|
||||
end
|
||||
end
|
||||
|
||||
def set_cache_control_defaults
|
||||
response.cache_control.replace(private: true, no_store: true)
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ class Auth::ChallengesController < ApplicationController
|
|||
|
||||
before_action :authenticate_user!
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def create
|
||||
|
|
|
@ -12,6 +12,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
|
||||
before_action :require_captcha_if_needed!, only: [:show]
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
|
@ -38,6 +39,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
show
|
||||
end
|
||||
|
||||
def redirect_to_app?
|
||||
truthy_param?(:redirect_to_app)
|
||||
end
|
||||
|
||||
helper_method :redirect_to_app?
|
||||
|
||||
private
|
||||
|
||||
def require_captcha_if_needed!
|
||||
|
@ -81,7 +88,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
end
|
||||
|
||||
def after_confirmation_path_for(_resource_name, user)
|
||||
if user.created_by_application && truthy_param?(:redirect_to_app)
|
||||
if user.created_by_application && redirect_to_app?
|
||||
user.created_by_application.confirmation_redirect_uri
|
||||
elsif user_signed_in?
|
||||
web_url('start')
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
||||
def self.provides_callback_for(provider)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::PasswordsController < Devise::PasswordsController
|
||||
skip_before_action :check_self_destruct!
|
||||
before_action :check_validity_of_reset_password_token, only: :edit
|
||||
before_action :set_body_classes
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
before_action :require_rules_acceptance!, only: :new
|
||||
before_action :set_registration_form_time, only: :new
|
||||
|
||||
skip_before_action :check_self_destruct!, only: [:edit, :update]
|
||||
skip_before_action :require_functional!, only: [:edit, :update]
|
||||
|
||||
def new
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class Auth::SessionsController < Devise::SessionsController
|
||||
layout 'auth'
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_no_authentication, only: [:create]
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :update_user_sign_in
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class BackupsController < ApplicationController
|
||||
include RoutingHelper
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
|
|
@ -7,6 +7,7 @@ module ExportControllerConcern
|
|||
before_action :authenticate_user!
|
||||
before_action :load_export
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ module TwoFactorAuthenticationConcern
|
|||
|
||||
included do
|
||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||
helper_method :webauthn_enabled?
|
||||
end
|
||||
|
||||
def two_factor_enabled?
|
||||
|
@ -87,4 +88,10 @@ module TwoFactorAuthenticationConcern
|
|||
|
||||
set_locale { render :two_factor }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def webauthn_enabled?
|
||||
@webauthn_enabled
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ class Settings::ExportsController < Settings::BaseController
|
|||
include Redisable
|
||||
include Lockable
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::LoginActivitiesController < Settings::BaseController
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def index
|
||||
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Settings
|
||||
module TwoFactorAuthentication
|
||||
class WebauthnCredentialsController < BaseController
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_otp_enabled
|
||||
|
|
|
@ -4,6 +4,7 @@ module Settings
|
|||
class TwoFactorAuthenticationMethodsController < BaseController
|
||||
include ChallengableConcern
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_challenge!, only: :disable
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
module DisputesHelper
|
||||
def strike_action_label(appeal)
|
||||
t(key_for_action(appeal),
|
||||
scope: 'admin.strikes.actions',
|
||||
name: content_tag(:span, appeal.strike.account.username, class: 'username'),
|
||||
target: content_tag(:span, appeal.account.username, class: 'target'))
|
||||
.html_safe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def key_for_action(appeal)
|
||||
AccountWarning.actions.slice(appeal.strike.action).keys.first
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,10 @@ module FormattingHelper
|
|||
TextFormatter.new(text, options).to_s
|
||||
end
|
||||
|
||||
def url_for_preview_card(preview_card)
|
||||
preview_card.url
|
||||
end
|
||||
|
||||
def extract_status_plain_text(status)
|
||||
PlainTextFormatter.new(status.text, status.local?).to_s
|
||||
end
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SelfDestructHelper
|
||||
def self.self_destruct?
|
||||
value = ENV.fetch('SELF_DESTRUCT', nil)
|
||||
value.present? && Rails.application.message_verifier('self-destruct').verify(value) == ENV['LOCAL_DOMAIN']
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||
false
|
||||
end
|
||||
|
||||
def self_destruct?
|
||||
SelfDestructHelper.self_destruct?
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default 'SvgrURL';
|
||||
export const ReactComponent = 'div';
|
|
@ -1,7 +1,7 @@
|
|||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import Button from '../button';
|
||||
import { Button } from '../button';
|
||||
|
||||
describe('<Button />', () => {
|
||||
it('renders a button element', () => {
|
||||
|
|
|
@ -15,10 +15,9 @@ import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
|||
import { me } from '../initial_state';
|
||||
|
||||
import { Avatar } from './avatar';
|
||||
import Button from './button';
|
||||
import { Button } from './button';
|
||||
import { FollowersCounter } from './counters';
|
||||
import { DisplayName } from './display_name';
|
||||
import { IconButton } from './icon_button';
|
||||
import { RelativeTimestamp } from './relative_timestamp';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -45,10 +44,7 @@ class Account extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
actionIcon: PropTypes.string,
|
||||
actionTitle: PropTypes.string,
|
||||
defaultAction: PropTypes.string,
|
||||
onActionClick: PropTypes.func,
|
||||
withBio: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
@ -76,12 +72,8 @@ class Account extends ImmutablePureComponent {
|
|||
this.props.onMuteNotifications(this.props.account, false);
|
||||
};
|
||||
|
||||
handleAction = () => {
|
||||
this.props.onActionClick(this.props.account);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, hidden, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
|
||||
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <EmptyAccount size={size} minimal={minimal} />;
|
||||
|
@ -98,9 +90,7 @@ class Account extends ImmutablePureComponent {
|
|||
|
||||
let buttons;
|
||||
|
||||
if (actionIcon && onActionClick) {
|
||||
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
||||
} else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
|
|
|
@ -7,6 +7,8 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as LinkIcon } from '@material-symbols/svg-600/outlined/link.svg';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
|
||||
|
@ -25,7 +27,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||
<div className={classNames('attachment-list', { compact })}>
|
||||
{!compact && (
|
||||
<div className='attachment-list__icon'>
|
||||
<Icon id='link' />
|
||||
<Icon id='link' icon={LinkIcon} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -36,7 +38,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||
return (
|
||||
<li key={attachment.get('id')}>
|
||||
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>
|
||||
{compact && <Icon id='link' />}
|
||||
{compact && <Icon id='link' icon={LinkIcon} />}
|
||||
{compact && ' ' }
|
||||
{displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}
|
||||
</a>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { PureComponent } from 'react';
|
|||
|
||||
import { assetHost } from 'mastodon/utils/config';
|
||||
|
||||
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
|
||||
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
|
||||
|
||||
export default class AutosuggestEmoji extends PureComponent {
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { ReactComponent as GroupsIcon } from '@material-design-icons/svg/outlined/group.svg';
|
||||
import { ReactComponent as PersonIcon } from '@material-design-icons/svg/outlined/person.svg';
|
||||
import { ReactComponent as SmartToyIcon } from '@material-design-icons/svg/outlined/smart_toy.svg';
|
||||
import { ReactComponent as GroupsIcon } from '@material-symbols/svg-600/outlined/group.svg';
|
||||
import { ReactComponent as PersonIcon } from '@material-symbols/svg-600/outlined/person.svg';
|
||||
import { ReactComponent as SmartToyIcon } from '@material-symbols/svg-600/outlined/smart_toy.svg';
|
||||
|
||||
|
||||
export const Badge = ({ icon, label, domain }) => (
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class Button extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.node,
|
||||
type: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
block: PropTypes.bool,
|
||||
secondary: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
type: 'button',
|
||||
};
|
||||
|
||||
handleClick = (e) => {
|
||||
if (!this.props.disabled && this.props.onClick) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
focus() {
|
||||
this.node.focus();
|
||||
}
|
||||
|
||||
render () {
|
||||
const className = classNames('button', this.props.className, {
|
||||
'button-secondary': this.props.secondary,
|
||||
'button--block': this.props.block,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.handleClick}
|
||||
ref={this.setRef}
|
||||
title={this.props.title}
|
||||
type={this.props.type}
|
||||
>
|
||||
{this.props.text || this.props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface BaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
block?: boolean;
|
||||
secondary?: boolean;
|
||||
text?: JSX.Element;
|
||||
}
|
||||
|
||||
interface PropsWithChildren extends BaseProps {
|
||||
text?: never;
|
||||
}
|
||||
|
||||
interface PropsWithText extends BaseProps {
|
||||
text: JSX.Element;
|
||||
children: never;
|
||||
}
|
||||
|
||||
type Props = PropsWithText | PropsWithChildren;
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
text,
|
||||
type = 'button',
|
||||
onClick,
|
||||
disabled,
|
||||
block,
|
||||
secondary,
|
||||
className,
|
||||
title,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const handleClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
|
||||
(e) => {
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
},
|
||||
[disabled, onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames('button', className, {
|
||||
'button-secondary': secondary,
|
||||
'button--block': block,
|
||||
})}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
type={type}
|
||||
{...props}
|
||||
>
|
||||
{text ?? children}
|
||||
</button>
|
||||
);
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
export const Check: React.FC = () => (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 20 20'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -1,62 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
export default class ColumnBackButton extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
multiColumn: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { router } = this.context;
|
||||
const { onClick } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else if (router.history.location?.state?.fromMastodon) {
|
||||
router.history.goBack();
|
||||
} else {
|
||||
router.history.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { multiColumn } = this.props;
|
||||
|
||||
const component = (
|
||||
<button onClick={this.handleClick} className='column-back-button'>
|
||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>
|
||||
);
|
||||
|
||||
if (multiColumn) {
|
||||
return component;
|
||||
} else {
|
||||
// The portal container and the component may be rendered to the DOM in
|
||||
// the same React render pass, so the container might not be available at
|
||||
// the time `render()` is called.
|
||||
const container = document.getElementById('tabs-bar__portal');
|
||||
if (container === null) {
|
||||
// The container wasn't available, force a re-render so that the
|
||||
// component can eventually be inserted in the container and not scroll
|
||||
// with the rest of the area.
|
||||
this.forceUpdate();
|
||||
return component;
|
||||
} else {
|
||||
return createPortal(component, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
|
||||
import { useAppHistory } from './router';
|
||||
|
||||
type OnClickCallback = () => void;
|
||||
|
||||
function useHandleClick(onClick?: OnClickCallback) {
|
||||
const history = useAppHistory();
|
||||
|
||||
return useCallback(() => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else if (history.location.state?.fromMastodon) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push('/');
|
||||
}
|
||||
}, [history, onClick]);
|
||||
}
|
||||
|
||||
export const ColumnBackButton: React.FC<{ onClick: OnClickCallback }> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
const handleClick = useHandleClick(onClick);
|
||||
|
||||
const component = (
|
||||
<button onClick={handleClick} className='column-back-button'>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
icon={ArrowBackIcon}
|
||||
className='column-back-button__icon'
|
||||
/>
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>
|
||||
);
|
||||
|
||||
return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
|
||||
};
|
||||
|
||||
export const ColumnBackButtonSlim: React.FC<{ onClick: OnClickCallback }> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
const handleClick = useHandleClick(onClick);
|
||||
|
||||
return (
|
||||
<div className='column-back-button--slim'>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
className='column-back-button column-back-button--slim-button'
|
||||
>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
icon={ArrowBackIcon}
|
||||
className='column-back-button__icon'
|
||||
/>
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import ColumnBackButton from './column_back_button';
|
||||
|
||||
export default class ColumnBackButtonSlim extends ColumnBackButton {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='column-back-button--slim'>
|
||||
<div role='button' tabIndex={0} onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
|
||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +1,21 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { ReactComponent as AddIcon } from '@material-symbols/svg-600/outlined/add.svg';
|
||||
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
|
||||
import { ReactComponent as ChevronLeftIcon } from '@material-symbols/svg-600/outlined/chevron_left.svg';
|
||||
import { ReactComponent as ChevronRightIcon } from '@material-symbols/svg-600/outlined/chevron_right.svg';
|
||||
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
|
||||
import { ReactComponent as TuneIcon } from '@material-symbols/svg-600/outlined/tune.svg';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
const messages = defineMessages({
|
||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
|
@ -18,7 +27,6 @@ const messages = defineMessages({
|
|||
class ColumnHeader extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -26,6 +34,7 @@ class ColumnHeader extends PureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
title: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
extraButton: PropTypes.node,
|
||||
|
@ -38,6 +47,7 @@ class ColumnHeader extends PureComponent {
|
|||
onClick: PropTypes.func,
|
||||
appendContent: PropTypes.node,
|
||||
collapseIssues: PropTypes.bool,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -63,12 +73,12 @@ class ColumnHeader extends PureComponent {
|
|||
};
|
||||
|
||||
handleBackClick = () => {
|
||||
const { router } = this.context;
|
||||
const { history } = this.props;
|
||||
|
||||
if (router.history.location?.state?.fromMastodon) {
|
||||
router.history.goBack();
|
||||
if (history.location?.state?.fromMastodon) {
|
||||
history.goBack();
|
||||
} else {
|
||||
router.history.push('/');
|
||||
history.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -78,15 +88,14 @@ class ColumnHeader extends PureComponent {
|
|||
|
||||
handlePin = () => {
|
||||
if (!this.props.pinned) {
|
||||
this.context.router.history.replace('/');
|
||||
this.props.history.replace('/');
|
||||
}
|
||||
|
||||
this.props.onPin();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { router } = this.context;
|
||||
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
|
||||
const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
|
@ -117,22 +126,22 @@ class ColumnHeader extends PureComponent {
|
|||
}
|
||||
|
||||
if (multiColumn && pinned) {
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
|
||||
moveButtons = (
|
||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
|
||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn && this.props.onPin) {
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
}
|
||||
|
||||
if (!pinned && ((multiColumn && router.history.location?.state?.fromMastodon) || showBackButton)) {
|
||||
if (!pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) {
|
||||
backButton = (
|
||||
<button onClick={this.handleBackClick} className='column-header__back-button'>
|
||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>
|
||||
);
|
||||
|
@ -156,21 +165,21 @@ class ColumnHeader extends PureComponent {
|
|||
onClick={this.handleToggleClick}
|
||||
>
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id='sliders' />
|
||||
<Icon id='sliders' icon={TuneIcon} />
|
||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||
</i>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const hasTitle = icon && title;
|
||||
const hasTitle = (icon || iconComponent) && title;
|
||||
|
||||
const component = (
|
||||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<button onClick={this.handleTitleClick}>
|
||||
<Icon id={icon} fixedWidth className='column-header__icon' />
|
||||
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
|
||||
{title}
|
||||
</button>
|
||||
)}
|
||||
|
@ -194,25 +203,15 @@ class ColumnHeader extends PureComponent {
|
|||
</div>
|
||||
);
|
||||
|
||||
if (multiColumn || placeholder) {
|
||||
if (placeholder) {
|
||||
return component;
|
||||
} else {
|
||||
// The portal container and the component may be rendered to the DOM in
|
||||
// the same React render pass, so the container might not be available at
|
||||
// the time `render()` is called.
|
||||
const container = document.getElementById('tabs-bar__portal');
|
||||
if (container === null) {
|
||||
// The container wasn't available, force a re-render so that the
|
||||
// component can eventually be inserted in the container and not scroll
|
||||
// with the rest of the area.
|
||||
this.forceUpdate();
|
||||
return component;
|
||||
} else {
|
||||
return createPortal(component, container);
|
||||
}
|
||||
return (<ButtonInTabsBar>
|
||||
{component}
|
||||
</ButtonInTabsBar>);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ColumnHeader);
|
||||
export default injectIntl(withRouter(ColumnHeader));
|
||||
|
|
|
@ -3,6 +3,8 @@ import { useCallback, useState } from 'react';
|
|||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
|
||||
|
||||
import { bannerSettings } from 'mastodon/settings';
|
||||
|
||||
import { IconButton } from './icon_button';
|
||||
|
@ -36,6 +38,7 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|||
<div className='dismissable-banner__action'>
|
||||
<IconButton
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
title={intl.formatMessage(messages.dismiss)}
|
||||
onClick={handleDismiss}
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,8 @@ import { useCallback } from 'react';
|
|||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/lock_open.svg';
|
||||
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -34,6 +36,7 @@ export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
|
|||
<IconButton
|
||||
active
|
||||
icon='unlock'
|
||||
iconComponent={LockOpenIcon}
|
||||
title={intl.formatMessage(messages.unblockDomain, { domain })}
|
||||
onClick={handleDomainUnblock}
|
||||
/>
|
||||
|
|
|
@ -2,13 +2,17 @@ import PropTypes from 'prop-types';
|
|||
import { PureComponent, cloneElement, Children } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import { CircularProgress } from "./circular_progress";
|
||||
import { CircularProgress } from 'mastodon/components/circular_progress';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||
|
@ -16,10 +20,6 @@ let id = 0;
|
|||
|
||||
class DropdownMenu extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
|
@ -159,15 +159,12 @@ class DropdownMenu extends PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default class Dropdown extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
class Dropdown extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
size: PropTypes.number,
|
||||
|
@ -183,6 +180,7 @@ export default class Dropdown extends PureComponent {
|
|||
renderItem: PropTypes.func,
|
||||
renderHeader: PropTypes.func,
|
||||
onItemClick: PropTypes.func,
|
||||
...WithRouterPropTypes
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -250,7 +248,7 @@ export default class Dropdown extends PureComponent {
|
|||
item.action();
|
||||
} else if (item && item.to) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(item.to);
|
||||
this.props.history.push(item.to);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -259,7 +257,7 @@ export default class Dropdown extends PureComponent {
|
|||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
return this.target?.buttonRef?.current;
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
|
@ -275,6 +273,7 @@ export default class Dropdown extends PureComponent {
|
|||
render () {
|
||||
const {
|
||||
icon,
|
||||
iconComponent,
|
||||
items,
|
||||
size,
|
||||
title,
|
||||
|
@ -295,9 +294,11 @@ export default class Dropdown extends PureComponent {
|
|||
onMouseDown: this.handleMouseDown,
|
||||
onKeyDown: this.handleButtonKeyDown,
|
||||
onKeyPress: this.handleKeyPress,
|
||||
ref: this.setTargetRef,
|
||||
}) : (
|
||||
<IconButton
|
||||
icon={!open ? icon : 'close'}
|
||||
iconComponent={!open ? iconComponent : CloseIcon}
|
||||
title={title}
|
||||
active={open}
|
||||
disabled={disabled}
|
||||
|
@ -306,14 +307,14 @@ export default class Dropdown extends PureComponent {
|
|||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
ref={this.setTargetRef}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span ref={this.setTargetRef}>
|
||||
{button}
|
||||
</span>
|
||||
{button}
|
||||
|
||||
<Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props, arrowProps, placement }) => (
|
||||
<div {...props}>
|
||||
|
@ -338,3 +339,5 @@ export default class Dropdown extends PureComponent {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(Dropdown);
|
||||
|
|
|
@ -5,6 +5,8 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ReactComponent as ArrowDropDownIcon } from '@material-symbols/svg-600/outlined/arrow_drop_down.svg';
|
||||
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import InlineAccount from 'mastodon/components/inline_account';
|
||||
|
@ -66,7 +68,7 @@ class EditedTimestamp extends PureComponent {
|
|||
return (
|
||||
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||
<button className='dropdown-menu__text-button'>
|
||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' />
|
||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
@ -1,20 +1,52 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLImageElement> {
|
||||
id: string;
|
||||
className?: string;
|
||||
fixedWidth?: boolean;
|
||||
import { ReactComponent as CheckBoxOutlineBlankIcon } from '@material-symbols/svg-600/outlined/check_box_outline_blank.svg';
|
||||
|
||||
interface SVGPropsWithTitle extends React.SVGProps<SVGSVGElement> {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export type IconProp = React.FC<SVGPropsWithTitle>;
|
||||
|
||||
interface Props extends React.SVGProps<SVGSVGElement> {
|
||||
children?: never;
|
||||
id: string;
|
||||
icon: IconProp;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const Icon: React.FC<Props> = ({
|
||||
id,
|
||||
icon: IconComponent,
|
||||
className,
|
||||
fixedWidth,
|
||||
title: titleProp,
|
||||
...other
|
||||
}) => (
|
||||
<i
|
||||
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!IconComponent) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
throw new Error(
|
||||
`<Icon id="${id}" className="${className}"> is missing an "icon" prop.`,
|
||||
);
|
||||
}
|
||||
|
||||
IconComponent = CheckBoxOutlineBlankIcon;
|
||||
}
|
||||
|
||||
const ariaHidden = titleProp ? undefined : true;
|
||||
const role = !ariaHidden ? 'img' : undefined;
|
||||
|
||||
// Set the title to an empty string to remove the built-in SVG one if any
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const title = titleProp || '';
|
||||
|
||||
return (
|
||||
<IconComponent
|
||||
className={classNames('icon', `icon-${id}`, className)}
|
||||
title={title}
|
||||
aria-hidden={ariaHidden}
|
||||
role={role}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import { PureComponent } from 'react';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { AnimatedNumber } from './animated_number';
|
||||
import type { IconProp } from './icon';
|
||||
import { Icon } from './icon';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
iconComponent: IconProp;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
|
||||
onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
|
||||
size: number;
|
||||
active: boolean;
|
||||
expanded?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
|
@ -32,8 +33,9 @@ interface States {
|
|||
deactivate: boolean;
|
||||
}
|
||||
export class IconButton extends PureComponent<Props, States> {
|
||||
buttonRef = createRef<HTMLButtonElement>();
|
||||
|
||||
static defaultProps = {
|
||||
size: 18,
|
||||
active: false,
|
||||
disabled: false,
|
||||
animate: false,
|
||||
|
@ -85,10 +87,6 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
|
||||
render() {
|
||||
const style = {
|
||||
fontSize: `${this.props.size}px`,
|
||||
width: `${this.props.size * 1.28571429}px`,
|
||||
height: `${this.props.size * 1.28571429}px`,
|
||||
lineHeight: `${this.props.size}px`,
|
||||
...this.props.style,
|
||||
...(this.props.active ? this.props.activeStyle : {}),
|
||||
};
|
||||
|
@ -99,6 +97,7 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
disabled,
|
||||
expanded,
|
||||
icon,
|
||||
iconComponent,
|
||||
inverted,
|
||||
overlay,
|
||||
tabIndex,
|
||||
|
@ -120,13 +119,9 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
'icon-button--with-counter': typeof counter !== 'undefined',
|
||||
});
|
||||
|
||||
if (typeof counter !== 'undefined') {
|
||||
style.width = 'auto';
|
||||
}
|
||||
|
||||
let contents = (
|
||||
<>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' />{' '}
|
||||
<Icon id={icon} icon={iconComponent} aria-hidden='true' />{' '}
|
||||
{typeof counter !== 'undefined' && (
|
||||
<span className='icon-button__counter'>
|
||||
<AnimatedNumber value={counter} />
|
||||
|
@ -158,6 +153,7 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
ref={this.buttonRef}
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
import type { IconProp } from './icon';
|
||||
import { Icon } from './icon';
|
||||
|
||||
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
icon: IconProp;
|
||||
count: number;
|
||||
issueBadge: boolean;
|
||||
className: string;
|
||||
}
|
||||
export const IconWithBadge: React.FC<Props> = ({
|
||||
id,
|
||||
icon,
|
||||
count,
|
||||
issueBadge,
|
||||
className,
|
||||
}) => (
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id={id} fixedWidth className={className} />
|
||||
<Icon id={id} icon={icon} className={className} />
|
||||
{count > 0 && (
|
||||
<i className='icon-with-badge__badge'>{formatNumber(count)}</i>
|
||||
)}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { useCallback } from 'react';
|
|||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -28,7 +30,7 @@ export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
|
|||
onClick={handleClick}
|
||||
aria-label={intl.formatMessage(messages.load_more)}
|
||||
>
|
||||
<Icon id='ellipsis-h' />
|
||||
<Icon id='ellipsis-h' icon={MoreHorizIcon} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
|||
import { is } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
|
@ -323,7 +324,7 @@ class MediaGallery extends PureComponent {
|
|||
</button>
|
||||
);
|
||||
} else if (visible) {
|
||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' overlay onClick={this.handleOpen} ariaHidden />;
|
||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' iconComponent={VisibilityOffIcon} overlay onClick={this.handleOpen} ariaHidden />;
|
||||
} else {
|
||||
spoilerButton = (
|
||||
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
||||
|
|
|
@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import { PureComponent } from 'react';
|
||||
|
||||
import 'wicg-inert';
|
||||
|
||||
import { multiply } from 'color-blend';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
export default class ModalRoot extends PureComponent {
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
class ModalRoot extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
|
@ -20,6 +19,7 @@ export default class ModalRoot extends PureComponent {
|
|||
b: PropTypes.number,
|
||||
}),
|
||||
ignoreFocus: PropTypes.bool,
|
||||
...WithOptionalRouterPropTypes,
|
||||
};
|
||||
|
||||
activeElement = this.props.children ? document.activeElement : null;
|
||||
|
@ -55,7 +55,7 @@ export default class ModalRoot extends PureComponent {
|
|||
componentDidMount () {
|
||||
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||
window.addEventListener('keydown', this.handleKeyDown, false);
|
||||
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
|
||||
this.history = this.props.history || createBrowserHistory();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
|
@ -156,3 +156,5 @@ export default class ModalRoot extends PureComponent {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
export default withOptionalRouter(ModalRoot);
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { PureComponent } from 'react';
|
||||
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
||||
import AccountNavigation from 'mastodon/features/account/navigation';
|
||||
import Trends from 'mastodon/features/getting_started/containers/trends_container';
|
||||
import { showTrends } from 'mastodon/initial_state';
|
||||
|
||||
const DefaultNavigation = () => (
|
||||
showTrends ? (
|
||||
<>
|
||||
<div className='flex-spacer' />
|
||||
<Trends />
|
||||
</>
|
||||
) : null
|
||||
);
|
||||
|
||||
class NavigationPortal extends PureComponent {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path='/@:acct' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/followers' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/following' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/media' exact component={AccountNavigation} />
|
||||
<Route component={DefaultNavigation} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export default withRouter(NavigationPortal);
|
|
@ -0,0 +1,25 @@
|
|||
import { Switch, Route } from 'react-router-dom';
|
||||
|
||||
import AccountNavigation from 'mastodon/features/account/navigation';
|
||||
import Trends from 'mastodon/features/getting_started/containers/trends_container';
|
||||
import { showTrends } from 'mastodon/initial_state';
|
||||
|
||||
const DefaultNavigation: React.FC = () =>
|
||||
showTrends ? (
|
||||
<>
|
||||
<div className='flex-spacer' />
|
||||
<Trends />
|
||||
</>
|
||||
) : null;
|
||||
|
||||
export const NavigationPortal: React.FC = () => (
|
||||
<Switch>
|
||||
<Route path='/@:acct' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/followers' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/following' exact component={AccountNavigation} />
|
||||
<Route path='/@:acct/media' exact component={AccountNavigation} />
|
||||
<Route component={DefaultNavigation} />
|
||||
</Switch>
|
||||
);
|
|
@ -5,6 +5,8 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ReactComponent as CancelPresentationIcon } from '@material-symbols/svg-600/outlined/cancel_presentation.svg';
|
||||
|
||||
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
|
@ -25,7 +27,7 @@ class PictureInPicturePlaceholder extends PureComponent {
|
|||
|
||||
return (
|
||||
<div className='picture-in-picture-placeholder' style={{ aspectRatio }} role='button' tabIndex={0} onClick={this.handleClick}>
|
||||
<Icon id='window-restore' />
|
||||
<Icon id='window-restore' icon={CancelPresentationIcon} />
|
||||
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
|
@ -192,7 +193,7 @@ class Poll extends ImmutablePureComponent {
|
|||
/>
|
||||
|
||||
{!!voted && <span className='poll__voted'>
|
||||
<Icon id='check' className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
|
||||
<Icon id='check' icon={CheckIcon} className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
|
||||
</span>}
|
||||
</label>
|
||||
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { Router as OriginalRouter, useHistory } from 'react-router';
|
||||
|
||||
import type {
|
||||
LocationDescriptor,
|
||||
LocationDescriptorObject,
|
||||
Path,
|
||||
} from 'history';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { Router as OriginalRouter } from 'react-router';
|
||||
|
||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||
|
||||
|
@ -11,34 +17,67 @@ interface MastodonLocationState {
|
|||
mastodonModalKey?: string;
|
||||
}
|
||||
|
||||
const browserHistory = createBrowserHistory<
|
||||
MastodonLocationState | undefined
|
||||
>();
|
||||
type LocationState = MastodonLocationState | null | undefined;
|
||||
|
||||
type HistoryPath = Path | LocationDescriptor<LocationState>;
|
||||
|
||||
const browserHistory = createBrowserHistory<LocationState>();
|
||||
const originalPush = browserHistory.push.bind(browserHistory);
|
||||
const originalReplace = browserHistory.replace.bind(browserHistory);
|
||||
|
||||
browserHistory.push = (path: string, state?: MastodonLocationState) => {
|
||||
state = state ?? {};
|
||||
state.fromMastodon = true;
|
||||
export function useAppHistory() {
|
||||
return useHistory<LocationState>();
|
||||
}
|
||||
|
||||
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
||||
originalPush(`/deck${path}`, state);
|
||||
} else {
|
||||
originalPush(path, state);
|
||||
function normalizePath(
|
||||
path: HistoryPath,
|
||||
state?: LocationState,
|
||||
): LocationDescriptorObject<LocationState> {
|
||||
const location = typeof path === 'string' ? { pathname: path } : { ...path };
|
||||
|
||||
if (location.state === undefined && state !== undefined) {
|
||||
location.state = state;
|
||||
} else if (
|
||||
location.state !== undefined &&
|
||||
state !== undefined &&
|
||||
process.env.NODE_ENV === 'development'
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'You should avoid providing a 2nd state argument to push when the 1st argument is a location-like object that already has state; it is ignored',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
layoutFromWindow() === 'multi-column' &&
|
||||
!location.pathname?.startsWith('/deck')
|
||||
) {
|
||||
location.pathname = `/deck${location.pathname}`;
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
browserHistory.push = (path: HistoryPath, state?: MastodonLocationState) => {
|
||||
const location = normalizePath(path, state);
|
||||
|
||||
location.state = location.state ?? {};
|
||||
location.state.fromMastodon = true;
|
||||
|
||||
originalPush(location);
|
||||
};
|
||||
|
||||
browserHistory.replace = (path: string, state?: MastodonLocationState) => {
|
||||
browserHistory.replace = (path: HistoryPath, state?: MastodonLocationState) => {
|
||||
const location = normalizePath(path, state);
|
||||
|
||||
if (!location.pathname) return;
|
||||
|
||||
if (browserHistory.location.state?.fromMastodon) {
|
||||
state = state ?? {};
|
||||
state.fromMastodon = true;
|
||||
location.state = location.state ?? {};
|
||||
location.state.fromMastodon = true;
|
||||
}
|
||||
|
||||
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
||||
originalReplace(`/deck${path}`, state);
|
||||
} else {
|
||||
originalReplace(path, state);
|
||||
}
|
||||
originalReplace(location);
|
||||
};
|
||||
|
||||
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import { Children, cloneElement, PureComponent } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -34,11 +35,32 @@ const mapStateToProps = (state, { scrollKey }) => {
|
|||
};
|
||||
};
|
||||
|
||||
class ScrollableList extends PureComponent {
|
||||
// This component only exists to be able to call useLocation()
|
||||
const IOArticleContainerWrapper = ({id, index, listLength, intersectionObserverWrapper, trackScroll, scrollKey, children}) => {
|
||||
const location = useLocation();
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
return (<IntersectionObserverArticleContainer
|
||||
id={id}
|
||||
index={index}
|
||||
listLength={listLength}
|
||||
intersectionObserverWrapper={intersectionObserverWrapper}
|
||||
saveHeightKey={trackScroll ? `${location.key}:${scrollKey}` : null}
|
||||
>
|
||||
{children}
|
||||
</IntersectionObserverArticleContainer>);
|
||||
};
|
||||
|
||||
IOArticleContainerWrapper.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
intersectionObserverWrapper: PropTypes.object.isRequired,
|
||||
trackScroll: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
class ScrollableList extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
|
@ -331,13 +353,14 @@ class ScrollableList extends PureComponent {
|
|||
{loadPending}
|
||||
|
||||
{Children.map(this.props.children, (child, index) => (
|
||||
<IntersectionObserverArticleContainer
|
||||
<IOArticleContainerWrapper
|
||||
key={child.key}
|
||||
id={child.key}
|
||||
index={index}
|
||||
listLength={childrenCount}
|
||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
|
||||
trackScroll={trackScroll}
|
||||
scrollKey={scrollKey}
|
||||
>
|
||||
{cloneElement(child, {
|
||||
getScrollPosition: this.getScrollPosition,
|
||||
|
@ -345,7 +368,7 @@ class ScrollableList extends PureComponent {
|
|||
cachedMediaWidth: this.state.cachedMediaWidth,
|
||||
cacheMediaWidth: this.cacheMediaWidth,
|
||||
})}
|
||||
</IntersectionObserverArticleContainer>
|
||||
</IOArticleContainerWrapper>
|
||||
))}
|
||||
|
||||
{loadMore}
|
||||
|
|
|
@ -7,10 +7,15 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as AlternateEmailIcon } from '@material-symbols/svg-600/outlined/alternate_email.svg';
|
||||
import { ReactComponent as PushPinIcon } from '@material-symbols/svg-600/outlined/push_pin.svg';
|
||||
import { ReactComponent as RepeatIcon } from '@material-symbols/svg-600/outlined/repeat.svg';
|
||||
import { ReactComponent as ReplyIcon } from '@material-symbols/svg-600/outlined/reply.svg';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
import Card from '../features/status/components/card';
|
||||
// We use the component (and not the container) since we do not want
|
||||
|
@ -26,6 +31,7 @@ import { getHashtagBarForStatus } from './hashtag_bar';
|
|||
import { RelativeTimestamp } from './relative_timestamp';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import StatusContent from './status_content';
|
||||
import { VisibilityIcon } from './visibility_icon';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
|
@ -72,10 +78,6 @@ const messages = defineMessages({
|
|||
|
||||
class Status extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
account: ImmutablePropTypes.map,
|
||||
|
@ -116,6 +118,7 @@ class Status extends ImmutablePureComponent {
|
|||
inUse: PropTypes.bool,
|
||||
available: PropTypes.bool,
|
||||
}),
|
||||
...WithOptionalRouterPropTypes,
|
||||
};
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
|
@ -258,7 +261,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleHotkeyReply = e => {
|
||||
e.preventDefault();
|
||||
this.props.onReply(this._properStatus(), this.context.router.history);
|
||||
this.props.onReply(this._properStatus(), this.props.history);
|
||||
};
|
||||
|
||||
handleHotkeyFavourite = () => {
|
||||
|
@ -271,7 +274,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleHotkeyMention = e => {
|
||||
e.preventDefault();
|
||||
this.props.onMention(this._properStatus().get('account'), this.context.router.history);
|
||||
this.props.onMention(this._properStatus().get('account'), this.props.history);
|
||||
};
|
||||
|
||||
handleHotkeyOpen = () => {
|
||||
|
@ -280,14 +283,14 @@ class Status extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const { router } = this.context;
|
||||
const { history } = this.props;
|
||||
const status = this._properStatus();
|
||||
|
||||
if (!router) {
|
||||
if (!history) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||
};
|
||||
|
||||
handleHotkeyOpenProfile = () => {
|
||||
|
@ -295,14 +298,14 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
_openProfile = (proper = true) => {
|
||||
const { router } = this.context;
|
||||
const { history } = this.props;
|
||||
const status = proper ? this._properStatus() : this.props.status;
|
||||
|
||||
if (!router) {
|
||||
if (!history) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.history.push(`/@${status.getIn(['account', 'acct'])}`);
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}`);
|
||||
};
|
||||
|
||||
handleHotkeyMoveUp = e => {
|
||||
|
@ -407,7 +410,7 @@ class Status extends ImmutablePureComponent {
|
|||
if (featured) {
|
||||
prepend = (
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' icon={PushPinIcon} className='status__prepend-icon' /></div>
|
||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||
</div>
|
||||
);
|
||||
|
@ -416,7 +419,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
prepend = (
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='retweet' icon={RepeatIcon} className='status__prepend-icon' /></div>
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||
</div>
|
||||
);
|
||||
|
@ -428,7 +431,7 @@ class Status extends ImmutablePureComponent {
|
|||
} else if (status.get('visibility') === 'direct') {
|
||||
prepend = (
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='at' className='status__prepend-icon' fixedWidth /></div>
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='at' icon={AlternateEmailIcon} className='status__prepend-icon' /></div>
|
||||
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
|
||||
</div>
|
||||
);
|
||||
|
@ -437,7 +440,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
prepend = (
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='reply' className='status__prepend-icon' fixedWidth /></div>
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='reply' icon={ReplyIcon} className='status__prepend-icon' /></div>
|
||||
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||
</div>
|
||||
);
|
||||
|
@ -536,15 +539,6 @@ class Status extends ImmutablePureComponent {
|
|||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||
}
|
||||
|
||||
const visibilityIconInfo = {
|
||||
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
||||
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
|
||||
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
|
||||
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
|
||||
};
|
||||
|
||||
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||
|
||||
|
@ -559,7 +553,7 @@ class Status extends ImmutablePureComponent {
|
|||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div onClick={this.handleClick} className='status__info'>
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
||||
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||
</a>
|
||||
|
||||
|
@ -596,4 +590,4 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default injectIntl(Status);
|
||||
export default withOptionalRouter(injectIntl(Status));
|
||||
|
|
|
@ -3,12 +3,24 @@ import PropTypes from 'prop-types';
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ReactComponent as BookmarkIcon } from '@material-symbols/svg-600/outlined/bookmark-fill.svg';
|
||||
import { ReactComponent as BookmarkBorderIcon } from '@material-symbols/svg-600/outlined/bookmark.svg';
|
||||
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
|
||||
import { ReactComponent as RepeatIcon } from '@material-symbols/svg-600/outlined/repeat.svg';
|
||||
import { ReactComponent as ReplyIcon } from '@material-symbols/svg-600/outlined/reply.svg';
|
||||
import { ReactComponent as ReplyAllIcon } from '@material-symbols/svg-600/outlined/reply_all.svg';
|
||||
import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/star-fill.svg';
|
||||
import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg';
|
||||
import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg';
|
||||
|
||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
||||
import { me } from '../initial_state';
|
||||
|
@ -61,7 +73,6 @@ const mapStateToProps = (state, { status }) => ({
|
|||
class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -92,6 +103,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
withCounters: PropTypes.bool,
|
||||
scrollKey: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
|
@ -106,7 +118,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onReply(this.props.status, this.context.router.history);
|
||||
this.props.onReply(this.props.status, this.props.history);
|
||||
} else {
|
||||
this.props.onInteractionModal('reply', this.props.status);
|
||||
}
|
||||
|
@ -145,15 +157,15 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history);
|
||||
this.props.onDelete(this.props.status, this.props.history);
|
||||
};
|
||||
|
||||
handleRedraftClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
this.props.onDelete(this.props.status, this.props.history, true);
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.onEdit(this.props.status, this.context.router.history);
|
||||
this.props.onEdit(this.props.status, this.props.history);
|
||||
};
|
||||
|
||||
handlePinClick = () => {
|
||||
|
@ -161,11 +173,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleMentionClick = () => {
|
||||
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
||||
this.props.onMention(this.props.status.get('account'), this.props.history);
|
||||
};
|
||||
|
||||
handleDirectClick = () => {
|
||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||
this.props.onDirect(this.props.status.get('account'), this.props.history);
|
||||
};
|
||||
|
||||
handleMuteClick = () => {
|
||||
|
@ -205,7 +217,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleOpen = () => {
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
|
||||
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
|
||||
};
|
||||
|
||||
handleEmbed = () => {
|
||||
|
@ -334,12 +346,15 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
let replyIcon;
|
||||
let replyIconComponent;
|
||||
let replyTitle;
|
||||
if (status.get('in_reply_to_id', null) === null) {
|
||||
replyIcon = 'reply';
|
||||
replyIconComponent = ReplyIcon;
|
||||
replyTitle = intl.formatMessage(messages.reply);
|
||||
} else {
|
||||
replyIcon = 'reply-all';
|
||||
replyIconComponent = ReplyAllIcon;
|
||||
replyTitle = intl.formatMessage(messages.replyAll);
|
||||
}
|
||||
|
||||
|
@ -357,33 +372,33 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
const filterButton = this.props.onFilter && (
|
||||
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleHideClick} />
|
||||
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
|
||||
);
|
||||
|
||||
const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
|
||||
|
||||
return (
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
|
||||
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
|
||||
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
|
||||
|
||||
{filterButton}
|
||||
|
||||
<div className='status__action-bar__dropdown'>
|
||||
<DropdownMenuContainer
|
||||
scrollKey={scrollKey}
|
||||
status={status}
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
size={18}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuContainer
|
||||
scrollKey={scrollKey}
|
||||
status={status}
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(StatusActionBar));
|
||||
export default withRouter(connect(mapStateToProps)(injectIntl(StatusActionBar)));
|
||||
|
|
|
@ -4,11 +4,13 @@ import { PureComponent } from 'react';
|
|||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ReactComponent as ChevronRightIcon } from '@material-symbols/svg-600/outlined/chevron_right.svg';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import PollContainer from 'mastodon/containers/poll_container';
|
||||
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||
|
@ -68,7 +70,6 @@ const mapStateToProps = state => ({
|
|||
class StatusContent extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -83,6 +84,10 @@ class StatusContent extends PureComponent {
|
|||
onCollapsedToggle: PropTypes.func,
|
||||
languages: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object,
|
||||
// from react-router
|
||||
match: PropTypes.object.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -173,18 +178,18 @@ class StatusContent extends PureComponent {
|
|||
}
|
||||
|
||||
onMentionClick = (mention, e) => {
|
||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/@${mention.get('acct')}`);
|
||||
this.props.history.push(`/@${mention.get('acct')}`);
|
||||
}
|
||||
};
|
||||
|
||||
onHashtagClick = (hashtag, e) => {
|
||||
hashtag = hashtag.replace(/^#/, '');
|
||||
|
||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/tags/${hashtag}`);
|
||||
this.props.history.push(`/tags/${hashtag}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -247,14 +252,14 @@ class StatusContent extends PureComponent {
|
|||
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
const classNames = classnames('status__content', {
|
||||
'status__content--with-action': this.props.onClick && this.context.router,
|
||||
'status__content--with-action': this.props.onClick && this.props.history,
|
||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||
'status__content--collapsed': renderReadMore,
|
||||
});
|
||||
|
||||
const readMoreButton = renderReadMore && (
|
||||
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
|
||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth />
|
||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' icon={ChevronRightIcon} />
|
||||
</button>
|
||||
);
|
||||
|
||||
|
@ -324,4 +329,4 @@ class StatusContent extends PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(StatusContent));
|
||||
export default withRouter(connect(mapStateToProps)(injectIntl(StatusContent)));
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';
|
||||
|
||||
import { Icon } from './icon';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
@ -21,7 +23,7 @@ interface Props {
|
|||
}
|
||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||
<span className='verified-badge'>
|
||||
<Icon id='check' className='verified-badge__mark' />
|
||||
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
||||
<span dangerouslySetInnerHTML={stripRelMe(link)} />
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { ReactComponent as AlternateEmailIcon } from '@material-symbols/svg-600/outlined/alternate_email.svg';
|
||||
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
|
||||
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/lock_open.svg';
|
||||
import { ReactComponent as PublicIcon } from '@material-symbols/svg-600/outlined/public.svg';
|
||||
|
||||
import { Icon } from './icon';
|
||||
|
||||
type Visibility = 'public' | 'unlisted' | 'private' | 'direct';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
private_short: {
|
||||
id: 'privacy.private.short',
|
||||
defaultMessage: 'Followers only',
|
||||
},
|
||||
direct_short: {
|
||||
id: 'privacy.direct.short',
|
||||
defaultMessage: 'Mentioned people only',
|
||||
},
|
||||
});
|
||||
|
||||
export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
|
||||
visibility,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const visibilityIconInfo = {
|
||||
public: {
|
||||
icon: 'globe',
|
||||
iconComponent: PublicIcon,
|
||||
text: intl.formatMessage(messages.public_short),
|
||||
},
|
||||
unlisted: {
|
||||
icon: 'unlock',
|
||||
iconComponent: LockOpenIcon,
|
||||
text: intl.formatMessage(messages.unlisted_short),
|
||||
},
|
||||
private: {
|
||||
icon: 'lock',
|
||||
iconComponent: LockIcon,
|
||||
text: intl.formatMessage(messages.private_short),
|
||||
},
|
||||
direct: {
|
||||
icon: 'at',
|
||||
iconComponent: AlternateEmailIcon,
|
||||
text: intl.formatMessage(messages.direct_short),
|
||||
},
|
||||
};
|
||||
|
||||
const visibilityIcon = visibilityIconInfo[visibility];
|
||||
|
||||
return (
|
||||
<Icon
|
||||
id={visibilityIcon.icon}
|
||||
icon={visibilityIcon.iconComponent}
|
||||
title={visibilityIcon.text}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -10,6 +10,9 @@ import { List as ImmutableList } from 'immutable';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ReactComponent as ChevronRightIcon } from '@material-symbols/svg-600/outlined/chevron_right.svg';
|
||||
import { ReactComponent as ExpandMoreIcon } from '@material-symbols/svg-600/outlined/expand_more.svg';
|
||||
|
||||
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
@ -75,7 +78,7 @@ class Section extends PureComponent {
|
|||
return (
|
||||
<div className={classNames('about__section', { active: !collapsed })}>
|
||||
<div className='about__section__title' role='button' tabIndex={0} onClick={this.handleClick}>
|
||||
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} fixedWidth /> {title}
|
||||
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} icon={collapsed ? ChevronRightIcon : ExpandMoreIcon} /> {title}
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
|
|
|
@ -14,10 +14,6 @@ const messages = defineMessages({
|
|||
|
||||
class FeaturedTags extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
featuredTags: ImmutablePropTypes.list,
|
||||
|
|
|
@ -3,6 +3,9 @@ import { FormattedMessage } from 'react-intl';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';
|
||||
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
export default class FollowRequestNote extends ImmutablePureComponent {
|
||||
|
@ -22,12 +25,12 @@ export default class FollowRequestNote extends ImmutablePureComponent {
|
|||
|
||||
<div className='follow-request-banner__action'>
|
||||
<button type='button' className='button button-tertiary button--confirmation' onClick={onAuthorize}>
|
||||
<Icon id='check' fixedWidth />
|
||||
<Icon id='check' icon={CheckIcon} />
|
||||
<FormattedMessage id='follow_request.authorize' defaultMessage='Authorize' />
|
||||
</button>
|
||||
|
||||
<button type='button' className='button button-tertiary button--destructive' onClick={onReject}>
|
||||
<Icon id='times' fixedWidth />
|
||||
<Icon id='times' icon={CloseIcon} />
|
||||
<FormattedMessage id='follow_request.reject' defaultMessage='Reject' />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -4,14 +4,20 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';
|
||||
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
|
||||
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
|
||||
import { ReactComponent as NotificationsIcon } from '@material-symbols/svg-600/outlined/notifications-fill.svg';
|
||||
import { ReactComponent as NotificationsActiveIcon } from '@material-symbols/svg-600/outlined/notifications_active.svg';
|
||||
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
|
||||
import Button from 'mastodon/components/button';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
@ -19,6 +25,7 @@ import { ShortNumber } from 'mastodon/components/short_number';
|
|||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
import { autoPlayGif, me, domain } from 'mastodon/initial_state';
|
||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
import AccountNoteContainer from '../containers/account_note_container';
|
||||
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
|
||||
|
@ -83,11 +90,6 @@ const dateFormatOptions = {
|
|||
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
identity_props: ImmutablePropTypes.list,
|
||||
|
@ -111,6 +113,11 @@ class Header extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
domain: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
|
@ -173,25 +180,24 @@ class Header extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleHashtagClick = e => {
|
||||
const { router } = this.context;
|
||||
const { history } = this.props;
|
||||
const value = e.currentTarget.textContent.replace(/^#/, '');
|
||||
|
||||
if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
router.history.push(`/tags/${value}`);
|
||||
history.push(`/tags/${value}`);
|
||||
}
|
||||
};
|
||||
|
||||
handleMentionClick = e => {
|
||||
const { router } = this.context;
|
||||
const { onOpenURL } = this.props;
|
||||
const { history, onOpenURL } = this.props;
|
||||
|
||||
if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
const link = e.currentTarget;
|
||||
|
||||
onOpenURL(link.href, router.history, () => {
|
||||
onOpenURL(link.href, history, () => {
|
||||
window.location = link.href;
|
||||
});
|
||||
}
|
||||
|
@ -258,7 +264,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
||||
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsIcon : NotificationsActiveIcon} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||
}
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
|
@ -280,7 +286,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (account.get('locked')) {
|
||||
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
|
||||
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
|
||||
}
|
||||
|
||||
if (signedIn && account.get('id') !== me) {
|
||||
|
@ -410,7 +416,7 @@ class Header extends ImmutablePureComponent {
|
|||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -448,7 +454,7 @@ class Header extends ImmutablePureComponent {
|
|||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
|
||||
|
||||
<dd className='translate' title={pair.get('value_plain')}>
|
||||
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
||||
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
|
@ -492,4 +498,4 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default injectIntl(Header);
|
||||
export default withRouter(injectIntl(Header));
|
||||
|
|
|
@ -5,6 +5,10 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as AudiotrackIcon } from '@material-symbols/svg-600/outlined/music_note.svg';
|
||||
import { ReactComponent as PlayArrowIcon } from '@material-symbols/svg-600/outlined/play_arrow.svg';
|
||||
import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
|
||||
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
|
||||
|
@ -69,7 +73,7 @@ export default class MediaItem extends ImmutablePureComponent {
|
|||
if (!visible) {
|
||||
icon = (
|
||||
<span className='account-gallery__item__icons'>
|
||||
<Icon id='eye-slash' />
|
||||
<Icon id='eye-slash' icon={VisibilityOffIcon} />
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
|
@ -84,9 +88,9 @@ export default class MediaItem extends ImmutablePureComponent {
|
|||
);
|
||||
|
||||
if (attachment.get('type') === 'audio') {
|
||||
label = <Icon id='music' />;
|
||||
label = <Icon id='music' icon={AudiotrackIcon} />;
|
||||
} else {
|
||||
label = <Icon id='play' />;
|
||||
label = <Icon id='play' icon={PlayArrowIcon} />;
|
||||
}
|
||||
} else if (attachment.get('type') === 'image') {
|
||||
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
|
||||
|
|
|
@ -8,7 +8,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||
import { LoadMore } from 'mastodon/components/load_more';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||
|
@ -203,7 +203,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
<ColumnBackButton />
|
||||
|
||||
<ScrollContainer scrollKey='account_gallery'>
|
||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||
|
|
|
@ -2,17 +2,19 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
import InnerHeader from '../../account/components/header';
|
||||
|
||||
import MemorialNote from './memorial_note';
|
||||
import MovedNote from './moved_note';
|
||||
|
||||
export default class Header extends ImmutablePureComponent {
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
|
@ -34,10 +36,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
hideTabs: PropTypes.bool,
|
||||
domain: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
handleFollow = () => {
|
||||
|
@ -49,11 +48,11 @@ export default class Header extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleMention = () => {
|
||||
this.props.onMention(this.props.account, this.context.router.history);
|
||||
this.props.onMention(this.props.account, this.props.history);
|
||||
};
|
||||
|
||||
handleDirect = () => {
|
||||
this.props.onDirect(this.props.account, this.context.router.history);
|
||||
this.props.onDirect(this.props.account, this.props.history);
|
||||
};
|
||||
|
||||
handleReport = () => {
|
||||
|
@ -159,3 +158,5 @@ export default class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(Header);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { revealAccount } from 'mastodon/actions/accounts';
|
||||
import Button from 'mastodon/components/button';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
|
||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
|
|
|
@ -16,7 +16,7 @@ import { getAccountHidden } from 'mastodon/selectors';
|
|||
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
||||
import { fetchFeaturedTags } from '../../actions/featured_tags';
|
||||
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
import { ColumnBackButton } from '../../components/column_back_button';
|
||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import StatusList from '../../components/status_list';
|
||||
import Column from '../ui/components/column';
|
||||
|
@ -184,7 +184,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
<ColumnBackButton />
|
||||
|
||||
<StatusList
|
||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
||||
|
|
|
@ -7,6 +7,12 @@ import classNames from 'classnames';
|
|||
|
||||
import { is } from 'immutable';
|
||||
|
||||
import { ReactComponent as DownloadIcon } from '@material-symbols/svg-600/outlined/download.svg';
|
||||
import { ReactComponent as PauseIcon } from '@material-symbols/svg-600/outlined/pause.svg';
|
||||
import { ReactComponent as PlayArrowIcon } from '@material-symbols/svg-600/outlined/play_arrow.svg';
|
||||
import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
|
||||
import { ReactComponent as VolumeOffIcon } from '@material-symbols/svg-600/outlined/volume_off.svg';
|
||||
import { ReactComponent as VolumeUpIcon } from '@material-symbols/svg-600/outlined/volume_up.svg';
|
||||
import { throttle, debounce } from 'lodash';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
@ -554,8 +560,8 @@ class Audio extends PureComponent {
|
|||
<div className='video-player__controls active'>
|
||||
<div className='video-player__buttons-bar'>
|
||||
<div className='video-player__buttons left'>
|
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} icon={paused ? PlayArrowIcon : PauseIcon} /></button>
|
||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} icon={muted ? VolumeOffIcon : VolumeUpIcon} /></button>
|
||||
|
||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
|
||||
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} />
|
||||
|
@ -575,9 +581,9 @@ class Audio extends PureComponent {
|
|||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
{!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
||||
{!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>}
|
||||
<a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
|
||||
<Icon id={'download'} fixedWidth />
|
||||
<Icon id={'download'} icon={DownloadIcon} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,10 +6,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ReactComponent as BlockIcon } from '@material-symbols/svg-600/outlined/block-fill.svg';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
|
||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import { ColumnBackButtonSlim } from '../../components/column_back_button';
|
||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
|
@ -59,7 +60,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
|
||||
<Column bindToDocument={!multiColumn} icon='ban' iconComponent={BlockIcon} heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<ScrollableList
|
||||
scrollKey='blocks'
|
||||
|
|
|
@ -8,6 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ReactComponent as BookmarksIcon } from '@material-symbols/svg-600/outlined/bookmarks-fill.svg';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'mastodon/actions/bookmarks';
|
||||
|
@ -79,7 +80,8 @@ class Bookmarks extends ImmutablePureComponent {
|
|||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
|
||||
<ColumnHeader
|
||||
icon='bookmark'
|
||||
icon='bookmarks'
|
||||
iconComponent={BookmarksIcon}
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
|
|
|
@ -7,6 +7,8 @@ import { Helmet } from 'react-helmet';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ReactComponent as PeopleIcon } from '@material-symbols/svg-600/outlined/group.svg';
|
||||
|
||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
|
||||
|
@ -39,7 +41,6 @@ const mapStateToProps = (state, { columnId }) => {
|
|||
class CommunityTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -129,6 +130,7 @@ class CommunityTimeline extends PureComponent {
|
|||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='users'
|
||||
iconComponent={PeopleIcon}
|
||||
active={hasUnread}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={this.handlePin}
|
||||
|
|
|
@ -5,6 +5,8 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { ReactComponent as MenuIcon } from '@material-symbols/svg-600/outlined/menu.svg';
|
||||
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -60,7 +62,7 @@ class ActionBar extends PureComponent {
|
|||
return (
|
||||
<div className='compose__action-bar'>
|
||||
<div className='compose__action-bar-dropdown'>
|
||||
<DropdownMenuContainer items={menu} icon='bars' size={18} direction='right' />
|
||||
<DropdownMenuContainer items={menu} icon='bars' iconComponent={MenuIcon} size={24} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -7,13 +7,15 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
|
||||
import { length } from 'stringz';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
||||
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import Button from '../../../components/button';
|
||||
import { Button } from '../../../components/button';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import LanguageDropdown from '../containers/language_dropdown_container';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
|
@ -39,11 +41,6 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
|
@ -71,6 +68,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
isInReply: PropTypes.bool,
|
||||
singleColumn: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
...WithOptionalRouterPropTypes
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -114,7 +112,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(this.context.router ? this.context.router.history : null);
|
||||
this.props.onSubmit(this.props.history || null);
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
|
@ -232,7 +230,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
if (this.props.isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||
publishText = <><Icon id='lock' icon={LockIcon} /> {intl.formatMessage(messages.publish)}</>;
|
||||
} else {
|
||||
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
@ -318,4 +316,4 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default injectIntl(ComposeForm);
|
||||
export default withOptionalRouter(injectIntl(ComposeForm));
|
||||
|
|
|
@ -3,8 +3,11 @@ import { PureComponent } from 'react';
|
|||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { ReactComponent as InsertChartIcon } from '@material-symbols/svg-600/outlined/insert_chart.svg';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
|
||||
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
|
||||
|
@ -40,6 +43,7 @@ class PollButton extends PureComponent {
|
|||
<div className='compose-form__poll-button'>
|
||||
<IconButton
|
||||
icon='tasks'
|
||||
iconComponent={InsertChartIcon}
|
||||
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
|
|
|
@ -8,6 +8,9 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as AddIcon } from '@material-symbols/svg-600/outlined/add.svg';
|
||||
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
|
||||
|
||||
import AutosuggestInput from 'mastodon/components/autosuggest_input';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
@ -108,7 +111,7 @@ class OptionIntl extends PureComponent {
|
|||
</label>
|
||||
|
||||
<div className='poll__cancel'>
|
||||
<IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
|
||||
<IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' iconComponent={CloseIcon} onClick={this.handleOptionRemove} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
@ -164,7 +167,7 @@ class PollForm extends ImmutablePureComponent {
|
|||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
<button type='button' disabled={options.size >= 4} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
|
||||
<button type='button' disabled={options.size >= 4} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' icon={AddIcon} /> <FormattedMessage {...messages.add_option} /></button>
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
|
|
|
@ -5,9 +5,15 @@ import { injectIntl, defineMessages } from 'react-intl';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
import { ReactComponent as AlternateEmailIcon } from '@material-symbols/svg-600/outlined/alternate_email.svg';
|
||||
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
|
||||
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/lock_open.svg';
|
||||
import { ReactComponent as PublicIcon } from '@material-symbols/svg-600/outlined/public.svg';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
@ -123,7 +129,7 @@ class PrivacyDropdownMenu extends PureComponent {
|
|||
{items.map(item => (
|
||||
<div role='option' tabIndex={0} key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<Icon id={item.icon} fixedWidth />
|
||||
<Icon id={item.icon} icon={item.iconComponent} />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
|
@ -222,14 +228,14 @@ class PrivacyDropdown extends PureComponent {
|
|||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||
{ icon: 'globe', iconComponent: PublicIcon, value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'unlock', iconComponent: LockOpenIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'lock', iconComponent: LockIcon, value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||
];
|
||||
|
||||
if (!this.props.noDirect) {
|
||||
this.options.push(
|
||||
{ icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||
{ icon: 'at', iconComponent: AlternateEmailIcon, value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -253,25 +259,24 @@ class PrivacyDropdown extends PureComponent {
|
|||
const valueOption = this.options.find(item => item.value === value);
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
|
||||
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })} ref={this.setTargetRef}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
iconComponent={valueOption.iconComponent}
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
<Overlay show={open} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
|
|
|
@ -5,7 +5,10 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
|
||||
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
|
@ -17,14 +20,11 @@ const messages = defineMessages({
|
|||
|
||||
class ReplyIndicator extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
...WithOptionalRouterPropTypes,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
|
@ -34,7 +34,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
handleAccountClick = (e) => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||
this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -50,7 +50,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted /></div>
|
||||
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
|
@ -72,4 +72,4 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default injectIntl(ReplyIndicator);
|
||||
export default withOptionalRouter(injectIntl(ReplyIndicator));
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue