Merge remote-tracking branch 'glitch/main' (a21fe8687e
)
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
commit
78a9906dfe
|
@ -236,7 +236,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
// Common React utilities
|
// Common React utilities
|
||||||
{
|
{
|
||||||
pattern: '{classnames,react-helmet,react-router-dom}',
|
pattern: '{classnames,react-helmet,react-router,react-router-dom}',
|
||||||
group: 'external',
|
group: 'external',
|
||||||
position: 'before',
|
position: 'before',
|
||||||
},
|
},
|
||||||
|
|
|
@ -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: [
|
extends: [
|
||||||
'config:recommended',
|
'config:recommended',
|
||||||
':labels(dependencies)',
|
':labels(dependencies)',
|
||||||
':maintainLockFilesMonthly', // update non-direct dependencies monthly
|
|
||||||
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
|
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
|
||||||
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
|
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
|
||||||
],
|
],
|
||||||
|
|
|
@ -27,14 +27,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Set up Ruby environment
|
||||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
- name: Set up Ruby
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
ruby-version: .ruby-version
|
|
||||||
bundler-cache: true
|
|
||||||
|
|
||||||
- name: Run bundler-audit
|
- name: Run bundler-audit
|
||||||
run: bundle exec bundler-audit
|
run: bundle exec bundler-audit
|
||||||
|
|
|
@ -19,25 +19,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Set up Ruby environment
|
||||||
run: |
|
uses: ./.github/actions/setup-ruby
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libicu-dev libidn11-dev
|
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Javascript environment
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ./.github/actions/setup-javascript
|
||||||
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: Check for missing strings in English JSON
|
- name: Check for missing strings in English JSON
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -45,14 +45,8 @@ jobs:
|
||||||
run: sudo chown -R runner:docker .
|
run: sudo chown -R runner:docker .
|
||||||
|
|
||||||
# This is needed to run the normalize step
|
# This is needed to run the normalize step
|
||||||
- name: Install native Ruby dependencies
|
- name: Set up Ruby environment
|
||||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
- name: Set up Ruby
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
ruby-version: .ruby-version
|
|
||||||
bundler-cache: true
|
|
||||||
|
|
||||||
- name: Run i18n normalize task
|
- name: Run i18n normalize task
|
||||||
run: bundle exec i18n-tasks normalize
|
run: bundle exec i18n-tasks normalize
|
||||||
|
|
|
@ -35,14 +35,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Javascript environment
|
||||||
uses: actions/setup-node@v3
|
uses: ./.github/actions/setup-javascript
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install all yarn packages
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- uses: xt0rted/stylelint-problem-matcher@v1
|
- uses: xt0rted/stylelint-problem-matcher@v1
|
||||||
|
|
||||||
|
|
|
@ -30,16 +30,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Set up Ruby environment
|
||||||
run: |
|
uses: ./.github/actions/setup-ruby
|
||||||
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: Run haml-lint
|
- name: Run haml-lint
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -39,14 +39,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Javascript environment
|
||||||
uses: actions/setup-node@v3
|
uses: ./.github/actions/setup-javascript
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install all yarn packages
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- name: ESLint
|
- name: ESLint
|
||||||
run: yarn lint:js --max-warnings 0
|
run: yarn lint:js --max-warnings 0
|
||||||
|
|
|
@ -31,14 +31,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Javascript environment
|
||||||
uses: actions/setup-node@v3
|
uses: ./.github/actions/setup-javascript
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install all yarn packages
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
run: yarn lint:json
|
run: yarn lint:json
|
||||||
|
|
|
@ -31,14 +31,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Javascript environment
|
||||||
uses: actions/setup-node@v3
|
uses: ./.github/actions/setup-javascript
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install all yarn packages
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
run: yarn lint:md
|
run: yarn lint:md
|
||||||
|
|
|
@ -31,14 +31,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Set up Ruby environment
|
||||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
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 RuboCop Problem Matcher
|
- name: Set-up RuboCop Problem Matcher
|
||||||
uses: r7kamura/rubocop-problem-matchers-action@v1
|
uses: r7kamura/rubocop-problem-matchers-action@v1
|
||||||
|
|
|
@ -33,14 +33,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Javascript environment
|
||||||
uses: actions/setup-node@v3
|
uses: ./.github/actions/setup-javascript
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install all yarn packages
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
run: yarn lint:yml
|
run: yarn lint:yml
|
||||||
|
|
|
@ -35,14 +35,8 @@ jobs:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Javascript environment
|
||||||
uses: actions/setup-node@v3
|
uses: ./.github/actions/setup-javascript
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install all yarn packages
|
|
||||||
run: yarn --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Jest testing
|
- name: Jest testing
|
||||||
run: yarn jest --reporters github-actions summary
|
run: yarn jest --reporters github-actions summary
|
||||||
|
|
|
@ -72,16 +72,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Set up Ruby environment
|
||||||
run: |
|
uses: ./.github/actions/setup-ruby
|
||||||
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: Create database
|
- name: Create database
|
||||||
run: './bin/rails db:create'
|
run: './bin/rails db:create'
|
||||||
|
|
|
@ -71,16 +71,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Set up Ruby environment
|
||||||
run: |
|
uses: ./.github/actions/setup-ruby
|
||||||
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: Create database
|
- name: Create database
|
||||||
run: './bin/rails db:create'
|
run: './bin/rails db:create'
|
||||||
|
|
|
@ -34,24 +34,14 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Ruby environment
|
||||||
uses: actions/setup-node@v3
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
|
- name: Set up Javascript environment
|
||||||
|
uses: ./.github/actions/setup-javascript
|
||||||
with:
|
with:
|
||||||
cache: yarn
|
onlyProduction: 'true'
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- 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
|
- name: Precompile assets
|
||||||
# Previously had set this, but it's not supported
|
# Previously had set this, but it's not supported
|
||||||
# export NODE_OPTIONS=--openssl-legacy-provider
|
# export NODE_OPTIONS=--openssl-legacy-provider
|
||||||
|
@ -135,20 +125,11 @@ jobs:
|
||||||
path: './public'
|
path: './public'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Update package index
|
- name: Set up Ruby environment
|
||||||
run: sudo apt-get update
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
- 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
|
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby-version}}
|
ruby-version: ${{ matrix.ruby-version}}
|
||||||
bundler-cache: true
|
additional-system-dependencies: ffmpeg imagemagick libpam-dev
|
||||||
|
|
||||||
- name: Load database schema
|
- name: Load database schema
|
||||||
run: './bin/rails db:create db:schema:load db:seed'
|
run: './bin/rails db:create db:schema:load db:seed'
|
||||||
|
@ -210,28 +191,14 @@ jobs:
|
||||||
path: './public'
|
path: './public'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Update package index
|
- name: Set up Ruby environment
|
||||||
run: sudo apt-get update
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
- 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
|
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby-version}}
|
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
|
- name: Load database schema
|
||||||
run: './bin/rails db:create db:schema:load db:seed'
|
run: './bin/rails db:create db:schema:load db:seed'
|
||||||
|
@ -328,28 +295,14 @@ jobs:
|
||||||
path: './public'
|
path: './public'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Update package index
|
- name: Set up Ruby environment
|
||||||
run: sudo apt-get update
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
- 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
|
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby-version}}
|
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
|
- name: Load database schema
|
||||||
run: './bin/rails db:create db:schema:load db:seed'
|
run: './bin/rails db:create db:schema:load db:seed'
|
||||||
|
|
|
@ -1,33 +1,21 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `haml-lint --auto-gen-config`
|
# `haml-lint --auto-gen-config`
|
||||||
# on 2023-10-11 11:31:24 -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
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the lints are removed from the code base.
|
# one by one as the lints are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
# versions of Haml-Lint, may require this file to be generated again.
|
# versions of Haml-Lint, may require this file to be generated again.
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# Offense count: 946
|
# Offense count: 945
|
||||||
LineLength:
|
LineLength:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
# Offense count: 22
|
# Offense count: 10
|
||||||
UnnecessaryStringOutput:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# Offense count: 44
|
|
||||||
RuboCop:
|
RuboCop:
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# Offense count: 3
|
|
||||||
ViewLength:
|
|
||||||
exclude:
|
exclude:
|
||||||
- 'app/views/admin/accounts/show.html.haml'
|
- 'app/views/admin/accounts/_buttons.html.haml'
|
||||||
- 'app/views/admin/reports/show.html.haml'
|
- 'app/views/admin/accounts/_local_account.html.haml'
|
||||||
- 'app/views/disputes/strikes/show.html.haml'
|
- 'app/views/admin/accounts/index.html.haml'
|
||||||
|
- 'app/views/admin/roles/_form.html.haml'
|
||||||
# Offense count: 2
|
- 'app/views/layouts/application.html.haml'
|
||||||
IdNames:
|
|
||||||
exclude:
|
|
||||||
- 'app/views/oauth/authorizations/error.html.haml'
|
|
||||||
- 'app/views/shared/_error_messages.html.haml'
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
||||||
# using RuboCop version 1.56.1.
|
# using RuboCop version 1.57.1.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
|
@ -80,7 +80,6 @@ RSpec/AnyInstance:
|
||||||
- 'spec/controllers/admin/accounts_controller_spec.rb'
|
- 'spec/controllers/admin/accounts_controller_spec.rb'
|
||||||
- 'spec/controllers/admin/resets_controller_spec.rb'
|
- 'spec/controllers/admin/resets_controller_spec.rb'
|
||||||
- 'spec/controllers/admin/settings/branding_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/auth/sessions_controller_spec.rb'
|
||||||
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
|
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
|
||||||
- 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'
|
- 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'
|
||||||
|
@ -180,7 +179,6 @@ RSpec/LetSetup:
|
||||||
|
|
||||||
RSpec/MessageChain:
|
RSpec/MessageChain:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'spec/controllers/api/v1/media_controller_spec.rb'
|
|
||||||
- 'spec/models/concerns/remotable_spec.rb'
|
- 'spec/models/concerns/remotable_spec.rb'
|
||||||
- 'spec/models/session_activation_spec.rb'
|
- 'spec/models/session_activation_spec.rb'
|
||||||
- 'spec/models/setting_spec.rb'
|
- 'spec/models/setting_spec.rb'
|
||||||
|
@ -219,19 +217,6 @@ Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/health_controller.rb'
|
- '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).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: Severity.
|
# Configuration parameters: Severity.
|
||||||
Rails/DuplicateAssociation:
|
Rails/DuplicateAssociation:
|
||||||
|
@ -273,7 +258,6 @@ Rails/LexicallyScopedActionFilter:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/auth/passwords_controller.rb'
|
- 'app/controllers/auth/passwords_controller.rb'
|
||||||
- 'app/controllers/auth/registrations_controller.rb'
|
- 'app/controllers/auth/registrations_controller.rb'
|
||||||
- 'app/controllers/auth/sessions_controller.rb'
|
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
Rails/NegateInclude:
|
Rails/NegateInclude:
|
||||||
|
@ -289,7 +273,6 @@ Rails/NegateInclude:
|
||||||
- 'app/models/custom_filter.rb'
|
- 'app/models/custom_filter.rb'
|
||||||
- 'app/services/activitypub/process_status_update_service.rb'
|
- 'app/services/activitypub/process_status_update_service.rb'
|
||||||
- 'app/services/fetch_link_card_service.rb'
|
- 'app/services/fetch_link_card_service.rb'
|
||||||
- 'app/services/search_service.rb'
|
|
||||||
- 'app/workers/web/push_notification_worker.rb'
|
- 'app/workers/web/push_notification_worker.rb'
|
||||||
- 'lib/paperclip/color_extractor.rb'
|
- 'lib/paperclip/color_extractor.rb'
|
||||||
|
|
||||||
|
@ -309,24 +292,6 @@ Rails/RakeEnvironment:
|
||||||
- 'lib/tasks/repo.rake'
|
- 'lib/tasks/repo.rake'
|
||||||
- 'lib/tasks/statistics.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.
|
# 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
|
# 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:
|
Rails/SkipsModelValidations:
|
||||||
|
@ -379,31 +344,6 @@ Rails/SkipsModelValidations:
|
||||||
- 'spec/services/follow_service_spec.rb'
|
- 'spec/services/follow_service_spec.rb'
|
||||||
- 'spec/services/update_account_service_spec.rb'
|
- 'spec/services/update_account_service_spec.rb'
|
||||||
|
|
||||||
# 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/20171210213213_add_local_only_flag_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.
|
# Configuration parameters: Include.
|
||||||
# Include: app/models/**/*.rb
|
# Include: app/models/**/*.rb
|
||||||
Rails/UniqueValidationWithoutIndex:
|
Rails/UniqueValidationWithoutIndex:
|
||||||
|
@ -467,7 +407,7 @@ Style/CaseEquality:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/trusted_proxies.rb'
|
- 'config/initializers/trusted_proxies.rb'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||||
# AllowedMethods: ==, equal?, eql?
|
# AllowedMethods: ==, equal?, eql?
|
||||||
Style/ClassEqualityComparison:
|
Style/ClassEqualityComparison:
|
||||||
|
@ -675,7 +615,6 @@ Style/RedundantReturn:
|
||||||
Style/SafeNavigation:
|
Style/SafeNavigation:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/concerns/account_finder_concern.rb'
|
- 'app/models/concerns/account_finder_concern.rb'
|
||||||
- 'app/models/status.rb'
|
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: EnforcedStyle.
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
|
2329
CHANGELOG.md
2329
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
|
@ -38,7 +38,7 @@ RUN --mount=type=cache,id=apt,target=/var/cache/apt,sharing=private \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libreadline8 \
|
libreadline8 \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libyaml-0-2 \
|
libyaml-dev \
|
||||||
python3 \
|
python3 \
|
||||||
shared-mime-info \
|
shared-mime-info \
|
||||||
zlib1g-dev
|
zlib1g-dev
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -4,7 +4,7 @@ source 'https://rubygems.org'
|
||||||
ruby '>= 3.0.0'
|
ruby '>= 3.0.0'
|
||||||
|
|
||||||
gem 'puma', '~> 6.3'
|
gem 'puma', '~> 6.3'
|
||||||
gem 'rails', '~> 7.0'
|
gem 'rails', '~> 7.1.1'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 1.2'
|
gem 'thor', '~> 1.2'
|
||||||
gem 'rack', '~> 2.2.7'
|
gem 'rack', '~> 2.2.7'
|
||||||
|
|
173
Gemfile.lock
173
Gemfile.lock
|
@ -39,75 +39,83 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.0.8)
|
actioncable (7.1.1)
|
||||||
actionpack (= 7.0.8)
|
actionpack (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.8)
|
zeitwerk (~> 2.6)
|
||||||
actionpack (= 7.0.8)
|
actionmailbox (7.1.1)
|
||||||
activejob (= 7.0.8)
|
actionpack (= 7.1.1)
|
||||||
activerecord (= 7.0.8)
|
activejob (= 7.1.1)
|
||||||
activestorage (= 7.0.8)
|
activerecord (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activestorage (= 7.1.1)
|
||||||
|
activesupport (= 7.1.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.0.8)
|
actionmailer (7.1.1)
|
||||||
actionpack (= 7.0.8)
|
actionpack (= 7.1.1)
|
||||||
actionview (= 7.0.8)
|
actionview (= 7.1.1)
|
||||||
activejob (= 7.0.8)
|
activejob (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.0.8)
|
actionpack (7.1.1)
|
||||||
actionview (= 7.0.8)
|
actionview (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
nokogiri (>= 1.8.5)
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.0.8)
|
actiontext (7.1.1)
|
||||||
actionpack (= 7.0.8)
|
actionpack (= 7.1.1)
|
||||||
activerecord (= 7.0.8)
|
activerecord (= 7.1.1)
|
||||||
activestorage (= 7.0.8)
|
activestorage (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.8)
|
actionview (7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
active_model_serializers (0.10.14)
|
active_model_serializers (0.10.14)
|
||||||
actionpack (>= 4.1)
|
actionpack (>= 4.1)
|
||||||
activemodel (>= 4.1)
|
activemodel (>= 4.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (7.0.8)
|
activejob (7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.8)
|
activemodel (7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
activerecord (7.0.8)
|
activerecord (7.1.1)
|
||||||
activemodel (= 7.0.8)
|
activemodel (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
activestorage (7.0.8)
|
timeout (>= 0.4.0)
|
||||||
actionpack (= 7.0.8)
|
activestorage (7.1.1)
|
||||||
activejob (= 7.0.8)
|
actionpack (= 7.1.1)
|
||||||
activerecord (= 7.0.8)
|
activejob (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activerecord (= 7.1.1)
|
||||||
|
activesupport (= 7.1.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
activesupport (7.1.1)
|
||||||
activesupport (7.0.8)
|
base64
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
mutex_m
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.5)
|
addressable (2.8.5)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
@ -158,6 +166,7 @@ GEM
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
|
bigdecimal (3.1.4)
|
||||||
bindata (2.4.15)
|
bindata (2.4.15)
|
||||||
binding_of_caller (1.0.0)
|
binding_of_caller (1.0.0)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
|
@ -237,6 +246,8 @@ GEM
|
||||||
dotenv-rails (2.8.1)
|
dotenv-rails (2.8.1)
|
||||||
dotenv (= 2.8.1)
|
dotenv (= 2.8.1)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
|
drb (2.1.1)
|
||||||
|
ruby2_keywords
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
elasticsearch (7.13.3)
|
elasticsearch (7.13.3)
|
||||||
elasticsearch-api (= 7.13.3)
|
elasticsearch-api (= 7.13.3)
|
||||||
|
@ -306,8 +317,8 @@ GEM
|
||||||
fuubar (2.5.1)
|
fuubar (2.5.1)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (1.1.0)
|
globalid (1.2.1)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 6.1)
|
||||||
haml (6.2.0)
|
haml (6.2.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
|
@ -358,7 +369,11 @@ GEM
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
idn-ruby (0.1.5)
|
idn-ruby (0.1.5)
|
||||||
|
io-console (0.6.0)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
|
irb (1.8.1)
|
||||||
|
rdoc
|
||||||
|
reline (>= 0.3.8)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.6.3)
|
||||||
json-canonicalization (0.3.2)
|
json-canonicalization (0.3.2)
|
||||||
|
@ -435,7 +450,6 @@ GEM
|
||||||
azure-storage-blob (~> 2.0.1)
|
azure-storage-blob (~> 2.0.1)
|
||||||
hashie (~> 5.0)
|
hashie (~> 5.0)
|
||||||
memory_profiler (1.0.1)
|
memory_profiler (1.0.1)
|
||||||
method_source (1.0.0)
|
|
||||||
mime-types (3.5.1)
|
mime-types (3.5.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2023.0808)
|
mime-types-data (3.2023.0808)
|
||||||
|
@ -445,11 +459,12 @@ GEM
|
||||||
msgpack (1.7.1)
|
msgpack (1.7.1)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
|
mutex_m (0.1.2)
|
||||||
net-http (0.3.2)
|
net-http (0.3.2)
|
||||||
uri
|
uri
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.3.7)
|
net-imap (0.4.1)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ldap (0.18.0)
|
net-ldap (0.18.0)
|
||||||
|
@ -457,7 +472,7 @@ GEM
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.1)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.4.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.15.4)
|
||||||
|
@ -513,6 +528,8 @@ GEM
|
||||||
net-smtp
|
net-smtp
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
private_address_check (0.5.0)
|
private_address_check (0.5.0)
|
||||||
|
psych (5.1.1)
|
||||||
|
stringio
|
||||||
public_suffix (5.0.3)
|
public_suffix (5.0.3)
|
||||||
puma (6.4.0)
|
puma (6.4.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
|
@ -535,22 +552,27 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
|
rack-session (1.0.1)
|
||||||
|
rack (< 3)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (7.0.8)
|
rackup (1.0.0)
|
||||||
actioncable (= 7.0.8)
|
rack (< 3)
|
||||||
actionmailbox (= 7.0.8)
|
webrick
|
||||||
actionmailer (= 7.0.8)
|
rails (7.1.1)
|
||||||
actionpack (= 7.0.8)
|
actioncable (= 7.1.1)
|
||||||
actiontext (= 7.0.8)
|
actionmailbox (= 7.1.1)
|
||||||
actionview (= 7.0.8)
|
actionmailer (= 7.1.1)
|
||||||
activejob (= 7.0.8)
|
actionpack (= 7.1.1)
|
||||||
activemodel (= 7.0.8)
|
actiontext (= 7.1.1)
|
||||||
activerecord (= 7.0.8)
|
actionview (= 7.1.1)
|
||||||
activestorage (= 7.0.8)
|
activejob (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activemodel (= 7.1.1)
|
||||||
|
activerecord (= 7.1.1)
|
||||||
|
activestorage (= 7.1.1)
|
||||||
|
activesupport (= 7.1.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.8)
|
railties (= 7.1.1)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
|
@ -565,19 +587,22 @@ GEM
|
||||||
rails-i18n (7.0.8)
|
rails-i18n (7.0.8)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
railties (7.0.8)
|
railties (7.1.1)
|
||||||
actionpack (= 7.0.8)
|
actionpack (= 7.1.1)
|
||||||
activesupport (= 7.0.8)
|
activesupport (= 7.1.1)
|
||||||
method_source
|
irb
|
||||||
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
rdf (3.2.11)
|
rdf (3.2.11)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.6.1)
|
rdf-normalize (0.6.1)
|
||||||
rdf (~> 3.2)
|
rdf (~> 3.2)
|
||||||
|
rdoc (6.5.0)
|
||||||
|
psych (>= 4.0.0)
|
||||||
redcarpet (3.6.0)
|
redcarpet (3.6.0)
|
||||||
redis (4.8.1)
|
redis (4.8.1)
|
||||||
redis-namespace (1.11.0)
|
redis-namespace (1.11.0)
|
||||||
|
@ -585,13 +610,15 @@ GEM
|
||||||
redlock (1.3.2)
|
redlock (1.3.2)
|
||||||
redis (>= 3.0.0, < 6.0)
|
redis (>= 3.0.0, < 6.0)
|
||||||
regexp_parser (2.8.2)
|
regexp_parser (2.8.2)
|
||||||
|
reline (0.3.9)
|
||||||
|
io-console (~> 0.5)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.6)
|
||||||
rotp (6.2.2)
|
rotp (6.3.0)
|
||||||
rouge (4.1.2)
|
rouge (4.1.2)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
|
@ -713,6 +740,7 @@ GEM
|
||||||
statsd-ruby (1.5.0)
|
statsd-ruby (1.5.0)
|
||||||
stoplight (3.0.2)
|
stoplight (3.0.2)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
|
stringio (3.0.8)
|
||||||
strong_migrations (0.8.0)
|
strong_migrations (0.8.0)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
swd (1.3.0)
|
swd (1.3.0)
|
||||||
|
@ -784,6 +812,7 @@ GEM
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
|
webrick (1.8.1)
|
||||||
websocket (1.2.10)
|
websocket (1.2.10)
|
||||||
websocket-driver (0.7.6)
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
|
@ -880,7 +909,7 @@ DEPENDENCIES
|
||||||
rack-attack (~> 6.6)
|
rack-attack (~> 6.6)
|
||||||
rack-cors (~> 2.0)
|
rack-cors (~> 2.0)
|
||||||
rack-test (~> 2.1)
|
rack-test (~> 2.1)
|
||||||
rails (~> 7.0)
|
rails (~> 7.1.1)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 7.0)
|
rails-i18n (~> 7.0)
|
||||||
rails-settings-cached (~> 0.6)!
|
rails-settings-cached (~> 0.6)!
|
||||||
|
@ -931,4 +960,4 @@ RUBY VERSION
|
||||||
ruby 3.2.2p53
|
ruby 3.2.2p53
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.13
|
2.4.20
|
||||||
|
|
|
@ -49,7 +49,7 @@ module Admin
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_instance
|
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
|
end
|
||||||
|
|
||||||
def set_instances
|
def set_instances
|
||||||
|
|
|
@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
|
||||||
include ThemingConcern
|
include ThemingConcern
|
||||||
include DatabaseHelper
|
include DatabaseHelper
|
||||||
include AuthorizedFetchHelper
|
include AuthorizedFetchHelper
|
||||||
|
include SelfDestructHelper
|
||||||
|
|
||||||
helper_method :current_account
|
helper_method :current_account
|
||||||
helper_method :current_session
|
helper_method :current_session
|
||||||
|
@ -41,6 +42,8 @@ class ApplicationController < ActionController::Base
|
||||||
service_unavailable
|
service_unavailable
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before_action :check_self_destruct!
|
||||||
|
|
||||||
before_action :store_referrer, except: :raise_not_found, if: :devise_controller?
|
before_action :store_referrer, except: :raise_not_found, if: :devise_controller?
|
||||||
before_action :require_functional!, if: :user_signed_in?
|
before_action :require_functional!, if: :user_signed_in?
|
||||||
|
|
||||||
|
@ -169,6 +172,15 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
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
|
def set_cache_control_defaults
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ class Auth::ChallengesController < ApplicationController
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
|
@ -13,6 +13,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
|
before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
|
||||||
before_action :require_captcha_if_needed!, only: [:show]
|
before_action :require_captcha_if_needed!, only: [:show]
|
||||||
|
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
|
||||||
def self.provides_callback_for(provider)
|
def self.provides_callback_for(provider)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::PasswordsController < Devise::PasswordsController
|
class Auth::PasswordsController < Devise::PasswordsController
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
before_action :check_validity_of_reset_password_token, only: :edit
|
before_action :check_validity_of_reset_password_token, only: :edit
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
|
@ -18,6 +18,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
before_action :require_rules_acceptance!, only: :new
|
before_action :require_rules_acceptance!, only: :new
|
||||||
before_action :set_registration_form_time, 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]
|
skip_before_action :require_functional!, only: [:edit, :update]
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
class Auth::SessionsController < Devise::SessionsController
|
class Auth::SessionsController < Devise::SessionsController
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :require_no_authentication, only: [:create]
|
skip_before_action :require_no_authentication, only: [:create]
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
skip_before_action :update_user_sign_in
|
skip_before_action :update_user_sign_in
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
class BackupsController < ApplicationController
|
class BackupsController < ApplicationController
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
|
@ -7,6 +7,7 @@ module ExportControllerConcern
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :load_export
|
before_action :load_export
|
||||||
|
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Settings::ExportsController < Settings::BaseController
|
||||||
include Redisable
|
include Redisable
|
||||||
include Lockable
|
include Lockable
|
||||||
|
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::LoginActivitiesController < Settings::BaseController
|
class Settings::LoginActivitiesController < Settings::BaseController
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
|
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
module Settings
|
module Settings
|
||||||
module TwoFactorAuthentication
|
module TwoFactorAuthentication
|
||||||
class WebauthnCredentialsController < BaseController
|
class WebauthnCredentialsController < BaseController
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :require_otp_enabled
|
before_action :require_otp_enabled
|
||||||
|
|
|
@ -4,6 +4,7 @@ module Settings
|
||||||
class TwoFactorAuthenticationMethodsController < BaseController
|
class TwoFactorAuthenticationMethodsController < BaseController
|
||||||
include ChallengableConcern
|
include ChallengableConcern
|
||||||
|
|
||||||
|
skip_before_action :check_self_destruct!
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :require_challenge!, only: :disable
|
before_action :require_challenge!, only: :disable
|
||||||
|
|
|
@ -9,6 +9,10 @@ module FormattingHelper
|
||||||
TextFormatter.new(text, options).to_s
|
TextFormatter.new(text, options).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def url_for_preview_card(preview_card)
|
||||||
|
preview_card.url
|
||||||
|
end
|
||||||
|
|
||||||
def extract_status_plain_text(status)
|
def extract_status_plain_text(status)
|
||||||
PlainTextFormatter.new(status.text, status.local?).to_s
|
PlainTextFormatter.new(status.text, status.local?).to_s
|
||||||
end
|
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 @@
|
||||||
// This file will be loaded on admin pages, regardless of theme.
|
// This file will be loaded on admin pages, regardless of theme.
|
||||||
|
|
||||||
import 'packs/public-path';
|
import 'packs/public-path';
|
||||||
import { delegate } from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
|
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ const setAnnouncementEndsAttributes = (target) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => {
|
Rails.delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => {
|
||||||
setAnnouncementEndsAttributes(target);
|
setAnnouncementEndsAttributes(target);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ const hideSelectAll = () => {
|
||||||
hiddenField.value = '0';
|
hiddenField.value = '0';
|
||||||
};
|
};
|
||||||
|
|
||||||
delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
||||||
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
||||||
|
|
||||||
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
|
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
|
||||||
|
@ -58,7 +58,7 @@ delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.batch-table__select-all button', 'click', () => {
|
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
|
||||||
const hiddenField = document.querySelector('#select_all_matching');
|
const hiddenField = document.querySelector('#select_all_matching');
|
||||||
const active = hiddenField.value === '1';
|
const active = hiddenField.value === '1';
|
||||||
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
|
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
|
||||||
|
@ -75,7 +75,7 @@ delegate(document, '.batch-table__select-all button', 'click', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, batchCheckboxClassName, 'change', () => {
|
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
|
||||||
const checkAllElement = document.querySelector('#batch_checkbox_all');
|
const checkAllElement = document.querySelector('#batch_checkbox_all');
|
||||||
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
||||||
|
|
||||||
|
@ -93,19 +93,19 @@ delegate(document, batchCheckboxClassName, 'change', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.media-spoiler-show-button', 'click', () => {
|
Rails.delegate(document, '.media-spoiler-show-button', 'click', () => {
|
||||||
[].forEach.call(document.querySelectorAll('button.media-spoiler'), (element) => {
|
[].forEach.call(document.querySelectorAll('button.media-spoiler'), (element) => {
|
||||||
element.click();
|
element.click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.media-spoiler-hide-button', 'click', () => {
|
Rails.delegate(document, '.media-spoiler-hide-button', 'click', () => {
|
||||||
[].forEach.call(document.querySelectorAll('.spoiler-button.spoiler-button--visible button'), (element) => {
|
[].forEach.call(document.querySelectorAll('.spoiler-button.spoiler-button--visible button'), (element) => {
|
||||||
element.click();
|
element.click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
|
Rails.delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
|
||||||
target.form.submit();
|
target.form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ const onDomainBlockSeverityChange = (target) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
|
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
|
||||||
|
|
||||||
const onEnableBootstrapTimelineAccountsChange = (target) => {
|
const onEnableBootstrapTimelineAccountsChange = (target) => {
|
||||||
const bootstrapTimelineAccountsField = document.querySelector('#form_admin_settings_bootstrap_timeline_accounts');
|
const bootstrapTimelineAccountsField = document.querySelector('#form_admin_settings_bootstrap_timeline_accounts');
|
||||||
|
@ -139,7 +139,7 @@ const onEnableBootstrapTimelineAccountsChange = (target) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
|
Rails.delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
|
||||||
|
|
||||||
const onChangeRegistrationMode = (target) => {
|
const onChangeRegistrationMode = (target) => {
|
||||||
const enabled = target.value === 'approved';
|
const enabled = target.value === 'approved';
|
||||||
|
@ -176,7 +176,7 @@ const convertLocalDatetimeToUTC = (value) => {
|
||||||
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
||||||
};
|
};
|
||||||
|
|
||||||
delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
|
Rails.delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
|
||||||
|
|
||||||
ready(() => {
|
ready(() => {
|
||||||
const domainBlockSeverityInput = document.getElementById('domain_block_severity');
|
const domainBlockSeverityInput = document.getElementById('domain_block_severity');
|
||||||
|
@ -213,7 +213,7 @@ ready(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, 'form', 'submit', ({ target }) => {
|
Rails.delegate(document, 'form', 'submit', ({ target }) => {
|
||||||
[].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => {
|
[].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => {
|
||||||
if (element.value && element.validity.valid) {
|
if (element.value && element.validity.valid) {
|
||||||
element.value = convertLocalDatetimeToUTC(element.value);
|
element.value = convertLocalDatetimeToUTC(element.value);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// This file will be loaded on settings pages, regardless of theme.
|
// This file will be loaded on settings pages, regardless of theme.
|
||||||
|
|
||||||
import 'packs/public-path';
|
import 'packs/public-path';
|
||||||
import { delegate } from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
|
|
||||||
delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
|
Rails.delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
|
||||||
const avatar = document.getElementById(target.id + '-preview');
|
const avatar = document.getElementById(target.id + '-preview');
|
||||||
const [file] = target.files || [];
|
const [file] = target.files || [];
|
||||||
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
|
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
|
||||||
|
@ -11,13 +11,13 @@ delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
|
||||||
avatar.src = url;
|
avatar.src = url;
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.input-copy input', 'click', ({ target }) => {
|
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
|
||||||
target.focus();
|
target.focus();
|
||||||
target.select();
|
target.select();
|
||||||
target.setSelectionRange(0, target.value.length);
|
target.setSelectionRange(0, target.value.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.input-copy button', 'click', ({ target }) => {
|
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
||||||
const input = target.parentNode.querySelector('.input-copy__wrapper input');
|
const input = target.parentNode.querySelector('.input-copy__wrapper input');
|
||||||
|
|
||||||
const oldReadOnly = input.readonly;
|
const oldReadOnly = input.readonly;
|
||||||
|
|
|
@ -106,7 +106,6 @@ export function fetchAccount(id) {
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
||||||
dispatch(importFetchedAccount(response.data));
|
dispatch(importFetchedAccount(response.data));
|
||||||
}).then(() => {
|
|
||||||
dispatch(fetchAccountSuccess());
|
dispatch(fetchAccountSuccess());
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchAccountFail(id, error));
|
dispatch(fetchAccountFail(id, error));
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
|
|
||||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';
|
|
||||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
|
|
||||||
dispatch(fetchAccountIdentityProofsRequest(accountId));
|
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
|
|
||||||
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
|
|
||||||
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchAccountIdentityProofsRequest = id => ({
|
|
||||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
|
|
||||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
|
|
||||||
accountId,
|
|
||||||
identity_proofs,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchAccountIdentityProofsFail = (accountId, err) => ({
|
|
||||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
|
|
||||||
accountId,
|
|
||||||
err,
|
|
||||||
skipNotFound: true,
|
|
||||||
});
|
|
|
@ -1,8 +1,8 @@
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
import emojify from 'flavours/glitch/features/emoji/emoji';
|
import emojify from '../../features/emoji/emoji';
|
||||||
import { autoHideCW } from 'flavours/glitch/utils/content_warning';
|
import { autoHideCW } from '../../utils/content_warning';
|
||||||
import { unescapeHTML } from 'flavours/glitch/utils/html';
|
import { unescapeHTML } from '../../utils/html';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,7 @@ export function reblogRequest(status) {
|
||||||
return {
|
return {
|
||||||
type: REBLOG_REQUEST,
|
type: REBLOG_REQUEST,
|
||||||
status: status,
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +91,7 @@ export function reblogSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: REBLOG_SUCCESS,
|
type: REBLOG_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +100,7 @@ export function reblogFail(status, error) {
|
||||||
type: REBLOG_FAIL,
|
type: REBLOG_FAIL,
|
||||||
status: status,
|
status: status,
|
||||||
error: error,
|
error: error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +108,7 @@ export function unreblogRequest(status) {
|
||||||
return {
|
return {
|
||||||
type: UNREBLOG_REQUEST,
|
type: UNREBLOG_REQUEST,
|
||||||
status: status,
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +116,7 @@ export function unreblogSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: UNREBLOG_SUCCESS,
|
type: UNREBLOG_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +125,7 @@ export function unreblogFail(status, error) {
|
||||||
type: UNREBLOG_FAIL,
|
type: UNREBLOG_FAIL,
|
||||||
status: status,
|
status: status,
|
||||||
error: error,
|
error: error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +159,7 @@ export function favouriteRequest(status) {
|
||||||
return {
|
return {
|
||||||
type: FAVOURITE_REQUEST,
|
type: FAVOURITE_REQUEST,
|
||||||
status: status,
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +167,7 @@ export function favouriteSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: FAVOURITE_SUCCESS,
|
type: FAVOURITE_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +176,7 @@ export function favouriteFail(status, error) {
|
||||||
type: FAVOURITE_FAIL,
|
type: FAVOURITE_FAIL,
|
||||||
status: status,
|
status: status,
|
||||||
error: error,
|
error: error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,6 +184,7 @@ export function unfavouriteRequest(status) {
|
||||||
return {
|
return {
|
||||||
type: UNFAVOURITE_REQUEST,
|
type: UNFAVOURITE_REQUEST,
|
||||||
status: status,
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,6 +192,7 @@ export function unfavouriteSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: UNFAVOURITE_SUCCESS,
|
type: UNFAVOURITE_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,6 +201,7 @@ export function unfavouriteFail(status, error) {
|
||||||
type: UNFAVOURITE_FAIL,
|
type: UNFAVOURITE_FAIL,
|
||||||
status: status,
|
status: status,
|
||||||
error: error,
|
error: error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,7 +211,7 @@ export function bookmark(status) {
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
|
||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(bookmarkSuccess(status));
|
dispatch(bookmarkSuccess(status, response.data));
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(bookmarkFail(status, error));
|
dispatch(bookmarkFail(status, error));
|
||||||
});
|
});
|
||||||
|
@ -212,7 +224,7 @@ export function unbookmark(status) {
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
|
||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(unbookmarkSuccess(status));
|
dispatch(unbookmarkSuccess(status, response.data));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unbookmarkFail(status, error));
|
dispatch(unbookmarkFail(status, error));
|
||||||
});
|
});
|
||||||
|
@ -226,10 +238,11 @@ export function bookmarkRequest(status) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bookmarkSuccess(status) {
|
export function bookmarkSuccess(status, response) {
|
||||||
return {
|
return {
|
||||||
type: BOOKMARK_SUCCESS,
|
type: BOOKMARK_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
|
response: response,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,10 +261,11 @@ export function unbookmarkRequest(status) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unbookmarkSuccess(status) {
|
export function unbookmarkSuccess(status, response) {
|
||||||
return {
|
return {
|
||||||
type: UNBOOKMARK_SUCCESS,
|
type: UNBOOKMARK_SUCCESS,
|
||||||
status: status,
|
status: status,
|
||||||
|
response: response,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,6 +458,7 @@ export function pinRequest(status) {
|
||||||
return {
|
return {
|
||||||
type: PIN_REQUEST,
|
type: PIN_REQUEST,
|
||||||
status,
|
status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,6 +466,7 @@ export function pinSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: PIN_SUCCESS,
|
type: PIN_SUCCESS,
|
||||||
status,
|
status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,6 +475,7 @@ export function pinFail(status, error) {
|
||||||
type: PIN_FAIL,
|
type: PIN_FAIL,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,6 +496,7 @@ export function unpinRequest(status) {
|
||||||
return {
|
return {
|
||||||
type: UNPIN_REQUEST,
|
type: UNPIN_REQUEST,
|
||||||
status,
|
status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -486,6 +504,7 @@ export function unpinSuccess(status) {
|
||||||
return {
|
return {
|
||||||
type: UNPIN_SUCCESS,
|
type: UNPIN_SUCCESS,
|
||||||
status,
|
status,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -494,5 +513,6 @@ export function unpinFail(status, error) {
|
||||||
type: UNPIN_FAIL,
|
type: UNPIN_FAIL,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { openModal } from './modal';
|
||||||
|
|
||||||
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||||
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||||
|
|
|
@ -5,10 +5,10 @@ import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { compareId } from 'flavours/glitch/compare_id';
|
import { compareId } from 'flavours/glitch/compare_id';
|
||||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||||
import { unescapeHTML } from 'flavours/glitch/utils/html';
|
|
||||||
import { requestNotificationPermission } from 'flavours/glitch/utils/notifications';
|
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
import { unescapeHTML } from '../utils/html';
|
||||||
|
import { requestNotificationPermission } from '../utils/notifications';
|
||||||
|
|
||||||
import { fetchFollowRequests, fetchRelationships } from './accounts';
|
import { fetchFollowRequests, fetchRelationships } from './accounts';
|
||||||
import {
|
import {
|
||||||
|
@ -21,9 +21,6 @@ import { submitMarkers } from './markers';
|
||||||
import { register as registerPushNotifications } from './push_notifications';
|
import { register as registerPushNotifications } from './push_notifications';
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||||
|
|
||||||
|
@ -65,7 +62,7 @@ defineMessages({
|
||||||
const fetchRelatedRelationships = (dispatch, notifications) => {
|
const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||||
const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
|
const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
|
||||||
|
|
||||||
if (accountIds > 0) {
|
if (accountIds.length > 0) {
|
||||||
dispatch(fetchRelationships(accountIds));
|
dispatch(fetchRelationships(accountIds));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -131,6 +128,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
|
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
|
||||||
|
|
||||||
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
|
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
|
||||||
|
|
||||||
notify.addEventListener('click', () => {
|
notify.addEventListener('click', () => {
|
||||||
window.focus();
|
window.focus();
|
||||||
notify.close();
|
notify.close();
|
||||||
|
@ -141,7 +139,6 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
|
|
||||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
|
|
||||||
|
|
||||||
const excludeTypesFromFilter = filter => {
|
const excludeTypesFromFilter = filter => {
|
||||||
const allTypes = ImmutableList([
|
const allTypes = ImmutableList([
|
||||||
'follow',
|
'follow',
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { me } from 'flavours/glitch/initial_state';
|
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
|
|
||||||
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
|
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
|
||||||
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
|
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
|
||||||
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
|
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
|
import { me } from '../../initial_state';
|
||||||
import { pushNotificationsSetting } from '../../settings';
|
import { pushNotificationsSetting } from '../../settings';
|
||||||
|
import { decode as decodeBase64 } from '../../utils/base64';
|
||||||
|
|
||||||
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
|
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
|
||||||
|
|
||||||
|
@ -10,13 +12,7 @@ const urlBase64ToUint8Array = (base64String) => {
|
||||||
.replace(/-/g, '+')
|
.replace(/-/g, '+')
|
||||||
.replace(/_/g, '/');
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
const rawData = window.atob(base64);
|
return decodeBase64(base64);
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return outputArray;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
|
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
|
||||||
|
@ -36,7 +32,7 @@ const subscribe = (registration) =>
|
||||||
const unsubscribe = ({ registration, subscription }) =>
|
const unsubscribe = ({ registration, subscription }) =>
|
||||||
subscription ? subscription.unsubscribe().then(() => registration) : registration;
|
subscription ? subscription.unsubscribe().then(() => registration) : registration;
|
||||||
|
|
||||||
const sendSubscriptionToBackend = (getState, subscription, me) => {
|
const sendSubscriptionToBackend = (subscription) => {
|
||||||
const params = { subscription };
|
const params = { subscription };
|
||||||
|
|
||||||
if (me) {
|
if (me) {
|
||||||
|
@ -46,7 +42,7 @@ const sendSubscriptionToBackend = (getState, subscription, me) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return api(getState).post('/api/web/push_subscriptions', params).then(response => response.data);
|
return api().post('/api/web/push_subscriptions', params).then(response => response.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
|
||||||
|
@ -55,7 +51,6 @@ const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager'
|
||||||
export function register () {
|
export function register () {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(setBrowserSupport(supportsPushNotifications));
|
dispatch(setBrowserSupport(supportsPushNotifications));
|
||||||
const me = getState().getIn(['meta', 'me']);
|
|
||||||
|
|
||||||
if (supportsPushNotifications) {
|
if (supportsPushNotifications) {
|
||||||
if (!getApplicationServerKey()) {
|
if (!getApplicationServerKey()) {
|
||||||
|
@ -79,13 +74,13 @@ export function register () {
|
||||||
} else {
|
} else {
|
||||||
// Something went wrong, try to subscribe again
|
// Something went wrong, try to subscribe again
|
||||||
return unsubscribe({ registration, subscription }).then(subscribe).then(
|
return unsubscribe({ registration, subscription }).then(subscribe).then(
|
||||||
subscription => sendSubscriptionToBackend(getState, subscription, me));
|
subscription => sendSubscriptionToBackend(subscription));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No subscription, try to subscribe
|
// No subscription, try to subscribe
|
||||||
return subscribe(registration).then(
|
return subscribe(registration).then(
|
||||||
subscription => sendSubscriptionToBackend(getState, subscription, me));
|
subscription => sendSubscriptionToBackend(subscription));
|
||||||
})
|
})
|
||||||
.then(subscription => {
|
.then(subscription => {
|
||||||
// If we got a PushSubscription (and not a subscription object from the backend)
|
// If we got a PushSubscription (and not a subscription object from the backend)
|
||||||
|
@ -128,10 +123,9 @@ export function saveSettings() {
|
||||||
const alerts = state.get('alerts');
|
const alerts = state.get('alerts');
|
||||||
const data = { alerts };
|
const data = { alerts };
|
||||||
|
|
||||||
api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
api().put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
|
||||||
data,
|
data,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const me = getState().getIn(['meta', 'me']);
|
|
||||||
if (me) {
|
if (me) {
|
||||||
pushNotificationsSetting.set(me, data);
|
pushNotificationsSetting.set(me, data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ const debouncedSave = debounce((dispatch, getState) => {
|
||||||
|
|
||||||
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
|
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
|
||||||
|
|
||||||
api(getState).put('/api/web/settings', { data })
|
api().put('/api/web/settings', { data })
|
||||||
.then(() => dispatch({ type: SETTING_SAVE }))
|
.then(() => dispatch({ type: SETTING_SAVE }))
|
||||||
.catch(error => dispatch(showAlertForError(error)));
|
.catch(error => dispatch(showAlertForError(error)));
|
||||||
}, 5000, { trailing: true });
|
}, 5000, { trailing: true });
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import { getLocale } from 'flavours/glitch/locales';
|
import { getLocale } from '../locales';
|
||||||
|
|
||||||
import { connectStream } from '../stream';
|
import { connectStream } from '../stream';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -68,8 +67,8 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
if (pollingId) {
|
if (pollingId) {
|
||||||
clearTimeout(pollingId);
|
// @ts-ignore
|
||||||
pollingId = null;
|
clearTimeout(pollingId); pollingId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.fillGaps) {
|
if (options.fillGaps) {
|
||||||
|
@ -86,8 +85,8 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onReceive (data) {
|
onReceive(data) {
|
||||||
switch(data.event) {
|
switch (data.event) {
|
||||||
case 'update':
|
case 'update':
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
import { fromJS } from 'immutable';
|
||||||
|
|
||||||
|
import type { StatusLike } from '../hashtag_bar';
|
||||||
|
import { computeHashtagBarForStatus } from '../hashtag_bar';
|
||||||
|
|
||||||
|
function createStatus(
|
||||||
|
content: string,
|
||||||
|
hashtags: string[],
|
||||||
|
hasMedia = false,
|
||||||
|
spoilerText?: string,
|
||||||
|
) {
|
||||||
|
return fromJS({
|
||||||
|
tags: hashtags.map((name) => ({ name })),
|
||||||
|
contentHtml: content,
|
||||||
|
media_attachments: hasMedia ? ['fakeMedia'] : [],
|
||||||
|
spoiler_text: spoilerText,
|
||||||
|
}) as unknown as StatusLike; // need to force the type here, as it is not properly defined
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('computeHashtagBarForStatus', () => {
|
||||||
|
it('does nothing when there are no tags', () => {
|
||||||
|
const status = createStatus('<p>Simple text</p>', []);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p>Simple text</p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays out of band hashtags in the bar', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p>Simple text <a href="test">#hashtag</a></p>',
|
||||||
|
['hashtag', 'test'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual(['test']);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p>Simple text <a href="test">#hashtag</a></p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not truncate the contents when the last child is a text node', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
|
||||||
|
['test'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extract tags from the last line', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
|
||||||
|
['hashtag'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual(['hashtag']);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p>Simple text</p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not include tags from content', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p>Simple text with a <a href="test">#hashtag</a></p><p><a href="test">#hashtag</a></p>',
|
||||||
|
['hashtag'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p>Simple text with a <a href="test">#hashtag</a></p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with one line status and hashtags', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>',
|
||||||
|
['hashtag', 'test'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('de-duplicate accentuated characters with case differences', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
|
||||||
|
['éaa'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual(['Éaa']);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p>Text</p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles server-side normalized tags with accentuated characters', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
|
||||||
|
['eaa'], // The server may normalize the hashtags in the `tags` attribute
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual(['Éaa']);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p>Text</p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not display in bar a hashtag in content with a case difference', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p>Text <a href="test">#Éaa</a></p><p><a href="test">#éaa</a></p>',
|
||||||
|
['éaa'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p>Text <a href="test">#Éaa</a></p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not modify a status with a line of hashtags only', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
|
||||||
|
['test', 'hashtag'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p>This is my content! <a href="test">#hashtag</a></p>',
|
||||||
|
['hashtag'],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p>This is my content! <a href="test">#hashtag</a></p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('puts the hashtags in the bar if a status content is only hashtags and has a media', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
|
||||||
|
['test', 'hashtag'],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual(['test', 'hashtag']);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(`""`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not use the hashtag bar if the status content is only hashtags, has a CW and a media', () => {
|
||||||
|
const status = createStatus(
|
||||||
|
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
|
||||||
|
['test', 'hashtag'],
|
||||||
|
true,
|
||||||
|
'My CW text',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { hashtagsInBar, statusContentProps } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
expect(hashtagsInBar).toEqual([]);
|
||||||
|
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||||
|
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,7 +6,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import { Skeleton } from 'flavours/glitch/components/skeleton';
|
import { Skeleton } from 'flavours/glitch/components/skeleton';
|
||||||
import { me } from 'flavours/glitch/initial_state';
|
|
||||||
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
import { Avatar } from './avatar';
|
||||||
import { DisplayName } from './display_name';
|
import { DisplayName } from './display_name';
|
||||||
|
@ -14,11 +15,10 @@ import { IconButton } from './icon_button';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
import { RelativeTimestamp } from './relative_timestamp';
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
|
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
|
||||||
|
@ -38,7 +38,6 @@ class Account extends ImmutablePureComponent {
|
||||||
onMuteNotifications: PropTypes.func.isRequired,
|
onMuteNotifications: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
small: PropTypes.bool,
|
|
||||||
actionIcon: PropTypes.string,
|
actionIcon: PropTypes.string,
|
||||||
actionTitle: PropTypes.string,
|
actionTitle: PropTypes.string,
|
||||||
defaultAction: PropTypes.string,
|
defaultAction: PropTypes.string,
|
||||||
|
@ -74,17 +73,7 @@ class Account extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size } = this.props;
|
||||||
account,
|
|
||||||
hidden,
|
|
||||||
intl,
|
|
||||||
small,
|
|
||||||
onActionClick,
|
|
||||||
actionIcon,
|
|
||||||
actionTitle,
|
|
||||||
defaultAction,
|
|
||||||
size,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return (
|
return (
|
||||||
|
@ -114,7 +103,7 @@ class Account extends ImmutablePureComponent {
|
||||||
if (actionIcon) {
|
if (actionIcon) {
|
||||||
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
||||||
}
|
}
|
||||||
} else if (account.get('id') !== me && !small && account.get('relationship', null) !== null) {
|
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||||
const following = account.getIn(['relationship', 'following']);
|
const following = account.getIn(['relationship', 'following']);
|
||||||
const requested = account.getIn(['relationship', 'requested']);
|
const requested = account.getIn(['relationship', 'requested']);
|
||||||
const blocking = account.getIn(['relationship', 'blocking']);
|
const blocking = account.getIn(['relationship', 'blocking']);
|
||||||
|
@ -151,24 +140,7 @@ class Account extends ImmutablePureComponent {
|
||||||
mute_expires_at = <div><RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></div>;
|
mute_expires_at = <div><RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return small ? (
|
return (
|
||||||
<Permalink
|
|
||||||
className='account small'
|
|
||||||
href={account.get('url')}
|
|
||||||
to={`/@${account.get('acct')}`}
|
|
||||||
>
|
|
||||||
<div className='account__avatar-wrapper'>
|
|
||||||
<Avatar
|
|
||||||
account={account}
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DisplayName
|
|
||||||
account={account}
|
|
||||||
inline
|
|
||||||
/>
|
|
||||||
</Permalink>
|
|
||||||
) : (
|
|
||||||
<div className='account'>
|
<div className='account'>
|
||||||
<div className='account__wrapper'>
|
<div className='account__wrapper'>
|
||||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { TransitionMotion, spring } from 'react-motion';
|
import { TransitionMotion, spring } from 'react-motion';
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light';
|
|
||||||
import { assetHost } from 'flavours/glitch/utils/config';
|
import { assetHost } from 'flavours/glitch/utils/config';
|
||||||
|
|
||||||
|
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
|
||||||
|
|
||||||
export default class AutosuggestEmoji extends PureComponent {
|
export default class AutosuggestEmoji extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -27,7 +28,7 @@ export default class AutosuggestEmoji extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji'>
|
<div className='autosuggest-emoji'>
|
||||||
<img
|
<img
|
||||||
className='emojione'
|
className='emojione'
|
||||||
src={url}
|
src={url}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import classNames from 'classnames';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||||
|
|
||||||
import AutosuggestEmoji from './autosuggest_emoji';
|
import AutosuggestEmoji from './autosuggest_emoji';
|
||||||
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
|
|
||||||
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||||
|
|
||||||
import AutosuggestEmoji from './autosuggest_emoji';
|
import AutosuggestEmoji from './autosuggest_emoji';
|
||||||
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useHovering } from 'flavours/glitch/hooks/useHovering';
|
import { useHovering } from '../hooks/useHovering';
|
||||||
import { autoPlayGif } from 'flavours/glitch/initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
import type { Account } from 'flavours/glitch/types/resources';
|
import type { Account } from '../types/resources';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined;
|
account: Account | undefined;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { PureComponent } from 'react';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
import { autoPlayGif } from 'flavours/glitch/initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
export default class AvatarComposite extends PureComponent {
|
export default class AvatarComposite extends PureComponent {
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useRef, useEffect } from 'react';
|
import { memo, useRef, useEffect } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { decode } from 'blurhash';
|
import { decode } from 'blurhash';
|
||||||
|
|
||||||
|
@ -44,6 +43,6 @@ const Blurhash: React.FC<Props> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MemoizedBlurhash = React.memo(Blurhash);
|
const MemoizedBlurhash = memo(Blurhash);
|
||||||
|
|
||||||
export { MemoizedBlurhash as Blurhash };
|
export { MemoizedBlurhash as Blurhash };
|
||||||
|
|
|
@ -1,53 +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,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
block: PropTypes.bool,
|
|
||||||
secondary: PropTypes.bool,
|
|
||||||
className: PropTypes.string,
|
|
||||||
title: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = (e) => {
|
|
||||||
if (!this.props.disabled) {
|
|
||||||
this.props.onClick(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = (c) => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.node.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let attrs = {
|
|
||||||
className: classNames('button', this.props.className, {
|
|
||||||
'button-secondary': this.props.secondary,
|
|
||||||
'button--block': this.props.block,
|
|
||||||
}),
|
|
||||||
disabled: this.props.disabled,
|
|
||||||
onClick: this.handleClick,
|
|
||||||
ref: this.setRef,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.props.title) attrs.title = this.props.title;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button {...attrs}>
|
|
||||||
{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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -24,12 +24,6 @@ export default class Column extends PureComponent {
|
||||||
scrollable = document.scrollingElement;
|
scrollable = document.scrollingElement;
|
||||||
} else {
|
} else {
|
||||||
scrollable = this.node.querySelector('.scrollable');
|
scrollable = this.node.querySelector('.scrollable');
|
||||||
|
|
||||||
// Some columns have nested `.scrollable` containers, with the outer one
|
|
||||||
// being a wrapper while the actual scrollable content is deeper.
|
|
||||||
if (scrollable.classList.contains('scrollable--flex')) {
|
|
||||||
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scrollable) {
|
if (!scrollable) {
|
||||||
|
|
|
@ -4,26 +4,25 @@ import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
|
export class ColumnBackButton extends PureComponent {
|
||||||
export default class ColumnBackButton extends PureComponent {
|
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
...WithRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
const { router } = this.context;
|
const { history } = this.props;
|
||||||
|
|
||||||
if (router.history.location?.state?.fromMastodon) {
|
if (history.location?.state?.fromMastodon) {
|
||||||
router.history.goBack();
|
history.goBack();
|
||||||
} else {
|
} else {
|
||||||
router.history.push('/');
|
history.push('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,3 +56,5 @@ export default class ColumnBackButton extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withRouter(ColumnBackButton);
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
export default class ColumnBackButtonSlim extends PureComponent {
|
class ColumnBackButtonSlim extends PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static propTypes = {
|
||||||
router: PropTypes.object,
|
...WithRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
const { router } = this.context;
|
const { location, history } = this.props;
|
||||||
|
|
||||||
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
|
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
|
||||||
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
|
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
|
||||||
if (router.route.location.key) {
|
if (location.key) {
|
||||||
router.history.goBack();
|
history.goBack();
|
||||||
} else {
|
} else {
|
||||||
router.history.push('/');
|
history.push('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,5 +35,6 @@ export default class ColumnBackButtonSlim extends PureComponent {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withRouter(ColumnBackButtonSlim);
|
||||||
|
|
|
@ -5,8 +5,10 @@ import { createPortal } from 'react-dom';
|
||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||||
|
@ -18,7 +20,6 @@ const messages = defineMessages({
|
||||||
class ColumnHeader extends PureComponent {
|
class ColumnHeader extends PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
|
||||||
identity: PropTypes.object,
|
identity: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ class ColumnHeader extends PureComponent {
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
appendContent: PropTypes.node,
|
appendContent: PropTypes.node,
|
||||||
collapseIssues: PropTypes.bool,
|
collapseIssues: PropTypes.bool,
|
||||||
|
...WithRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -63,12 +65,12 @@ class ColumnHeader extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleBackClick = () => {
|
handleBackClick = () => {
|
||||||
const { router } = this.context;
|
const { history } = this.props;
|
||||||
|
|
||||||
if (router.history.location?.state?.fromMastodon) {
|
if (history.location?.state?.fromMastodon) {
|
||||||
router.history.goBack();
|
history.goBack();
|
||||||
} else {
|
} else {
|
||||||
router.history.push('/');
|
history.push('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,15 +80,14 @@ class ColumnHeader extends PureComponent {
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
if (!this.props.pinned) {
|
if (!this.props.pinned) {
|
||||||
this.context.router.history.replace('/');
|
this.props.history.replace('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onPin();
|
this.props.onPin();
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { router } = this.context;
|
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
|
||||||
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
|
|
||||||
const { collapsed, animating } = this.state;
|
const { collapsed, animating } = this.state;
|
||||||
|
|
||||||
const wrapperClassName = classNames('column-header__wrapper', {
|
const wrapperClassName = classNames('column-header__wrapper', {
|
||||||
|
@ -129,7 +130,7 @@ class ColumnHeader extends PureComponent {
|
||||||
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' /> <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 = (
|
backButton = (
|
||||||
<button onClick={this.handleBackClick} className='column-header__back-button'>
|
<button onClick={this.handleBackClick} className='column-header__back-button'>
|
||||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||||
|
@ -215,4 +216,4 @@ class ColumnHeader extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(ColumnHeader);
|
export default injectIntl(withRouter(ColumnHeader));
|
||||||
|
|
|
@ -4,9 +4,8 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import type { List } from 'immutable';
|
import type { List } from 'immutable';
|
||||||
|
|
||||||
import type { Account } from 'flavours/glitch/types/resources';
|
|
||||||
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
import type { Account } from '../types/resources';
|
||||||
|
|
||||||
import { Skeleton } from './skeleton';
|
import { Skeleton } from './skeleton';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,16 @@ import PropTypes from 'prop-types';
|
||||||
import { PureComponent, cloneElement, Children } from 'react';
|
import { PureComponent, cloneElement, Children } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import { CircularProgress } from "./circular_progress";
|
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
|
||||||
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
import { IconButton } from './icon_button';
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||||
|
@ -16,10 +19,6 @@ let id = 0;
|
||||||
|
|
||||||
class DropdownMenu extends PureComponent {
|
class DropdownMenu extends PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
|
@ -159,11 +158,7 @@ class DropdownMenu extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Dropdown extends PureComponent {
|
class Dropdown extends PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
@ -183,6 +178,7 @@ export default class Dropdown extends PureComponent {
|
||||||
renderItem: PropTypes.func,
|
renderItem: PropTypes.func,
|
||||||
renderHeader: PropTypes.func,
|
renderHeader: PropTypes.func,
|
||||||
onItemClick: PropTypes.func,
|
onItemClick: PropTypes.func,
|
||||||
|
...WithRouterPropTypes
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -250,7 +246,7 @@ export default class Dropdown extends PureComponent {
|
||||||
item.action();
|
item.action();
|
||||||
} else if (item && item.to) {
|
} else if (item && item.to) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.context.router.history.push(item.to);
|
this.props.history.push(item.to);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -338,3 +334,5 @@ export default class Dropdown extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withRouter(Dropdown);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
src: string;
|
src: string;
|
||||||
|
|
|
@ -25,11 +25,11 @@ class SilentErrorBoundary extends Component {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidCatch () {
|
componentDidCatch() {
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { List, Record } from 'immutable';
|
||||||
|
|
||||||
|
import { groupBy, minBy } from 'lodash';
|
||||||
|
|
||||||
|
import { getStatusContent } from './status_content';
|
||||||
|
|
||||||
|
// Fit on a single line on desktop
|
||||||
|
const VISIBLE_HASHTAGS = 3;
|
||||||
|
|
||||||
|
// Those types are not correct, they need to be replaced once this part of the state is typed
|
||||||
|
export type TagLike = Record<{ name: string }>;
|
||||||
|
export type StatusLike = Record<{
|
||||||
|
tags: List<TagLike>;
|
||||||
|
contentHTML: string;
|
||||||
|
media_attachments: List<unknown>;
|
||||||
|
spoiler_text?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function normalizeHashtag(hashtag: string) {
|
||||||
|
return (
|
||||||
|
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
|
||||||
|
).normalize('NFKC');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
|
||||||
|
return (
|
||||||
|
element instanceof HTMLAnchorElement &&
|
||||||
|
// it may be a <a> starting with a hashtag
|
||||||
|
(element.textContent?.[0] === '#' ||
|
||||||
|
// or a #<a>
|
||||||
|
element.previousSibling?.textContent?.[
|
||||||
|
element.previousSibling.textContent.length - 1
|
||||||
|
] === '#')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes duplicates from an hashtag list, case-insensitive, keeping only the best one
|
||||||
|
* "Best" here is defined by the one with the more casing difference (ie, the most camel-cased one)
|
||||||
|
* @param hashtags The list of hashtags
|
||||||
|
* @returns The input hashtags, but with only 1 occurence of each (case-insensitive)
|
||||||
|
*/
|
||||||
|
function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
|
||||||
|
const groups = groupBy(hashtags, (tag) =>
|
||||||
|
tag.normalize('NFKD').toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.values(groups).map((tags) => {
|
||||||
|
if (tags.length === 1) return tags[0];
|
||||||
|
|
||||||
|
// The best match is the one where we have the less difference between upper and lower case letter count
|
||||||
|
const best = minBy(tags, (tag) => {
|
||||||
|
const upperCase = Array.from(tag).reduce(
|
||||||
|
(acc, char) => (acc += char.toUpperCase() === char ? 1 : 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lowerCase = tag.length - upperCase;
|
||||||
|
|
||||||
|
return Math.abs(lowerCase - upperCase);
|
||||||
|
});
|
||||||
|
|
||||||
|
return best ?? tags[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the collator once, this is much more efficient
|
||||||
|
const collator = new Intl.Collator(undefined, {
|
||||||
|
sensitivity: 'base', // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
|
||||||
|
});
|
||||||
|
|
||||||
|
function localeAwareInclude(collection: string[], value: string) {
|
||||||
|
const normalizedValue = value.normalize('NFKC');
|
||||||
|
|
||||||
|
return !!collection.find(
|
||||||
|
(item) => collator.compare(item.normalize('NFKC'), normalizedValue) === 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use an intermediate function here to make it easier to test
|
||||||
|
export function computeHashtagBarForStatus(status: StatusLike): {
|
||||||
|
statusContentProps: { statusContent: string };
|
||||||
|
hashtagsInBar: string[];
|
||||||
|
} {
|
||||||
|
let statusContent = getStatusContent(status);
|
||||||
|
|
||||||
|
const tagNames = status
|
||||||
|
.get('tags')
|
||||||
|
.map((tag) => tag.get('name'))
|
||||||
|
.toJS();
|
||||||
|
|
||||||
|
// this is returned if we stop the processing early, it does not change what is displayed
|
||||||
|
const defaultResult = {
|
||||||
|
statusContentProps: { statusContent },
|
||||||
|
hashtagsInBar: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// return early if this status does not have any tags
|
||||||
|
if (tagNames.length === 0) return defaultResult;
|
||||||
|
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = statusContent.trim();
|
||||||
|
|
||||||
|
const lastChild = template.content.lastChild;
|
||||||
|
|
||||||
|
if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
|
||||||
|
|
||||||
|
template.content.removeChild(lastChild);
|
||||||
|
const contentWithoutLastLine = template;
|
||||||
|
|
||||||
|
// First, try to parse
|
||||||
|
const contentHashtags = Array.from(
|
||||||
|
contentWithoutLastLine.content.querySelectorAll<HTMLLinkElement>('a[href]'),
|
||||||
|
).reduce<string[]>((result, link) => {
|
||||||
|
if (isNodeLinkHashtag(link)) {
|
||||||
|
if (link.textContent) result.push(normalizeHashtag(link.textContent));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Now we parse the last line, and try to see if it only contains hashtags
|
||||||
|
const lastLineHashtags: string[] = [];
|
||||||
|
// try to see if the last line is only hashtags
|
||||||
|
let onlyHashtags = true;
|
||||||
|
|
||||||
|
const normalizedTagNames = tagNames.map((tag) => tag.normalize('NFKC'));
|
||||||
|
|
||||||
|
Array.from(lastChild.childNodes).forEach((node) => {
|
||||||
|
if (isNodeLinkHashtag(node) && node.textContent) {
|
||||||
|
const normalized = normalizeHashtag(node.textContent);
|
||||||
|
|
||||||
|
if (!localeAwareInclude(normalizedTagNames, normalized)) {
|
||||||
|
// stop here, this is not a real hashtag, so consider it as text
|
||||||
|
onlyHashtags = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localeAwareInclude(contentHashtags, normalized))
|
||||||
|
// only add it if it does not appear in the rest of the content
|
||||||
|
lastLineHashtags.push(normalized);
|
||||||
|
} else if (node.nodeType !== Node.TEXT_NODE || node.nodeValue?.trim()) {
|
||||||
|
// not a space
|
||||||
|
onlyHashtags = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hashtagsInBar = tagNames.filter((tag) => {
|
||||||
|
const normalizedTag = tag.normalize('NFKC');
|
||||||
|
// the tag does not appear at all in the status content, it is an out-of-band tag
|
||||||
|
return (
|
||||||
|
!localeAwareInclude(contentHashtags, normalizedTag) &&
|
||||||
|
!localeAwareInclude(lastLineHashtags, normalizedTag)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
|
||||||
|
const hasMedia = status.get('media_attachments').size > 0;
|
||||||
|
const hasSpoiler = !!status.get('spoiler_text');
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- due to https://github.com/microsoft/TypeScript/issues/9998
|
||||||
|
if (onlyHashtags && ((hasMedia && !hasSpoiler) || !isOnlyOneLine)) {
|
||||||
|
// if the last line only contains hashtags, and we either:
|
||||||
|
// - have other content in the status
|
||||||
|
// - dont have other content, but a media and no CW. If it has a CW, then we do not remove the content to avoid having an empty content behind the CW button
|
||||||
|
statusContent = contentWithoutLastLine.innerHTML;
|
||||||
|
// and add the tags to the bar
|
||||||
|
hashtagsInBar.push(...lastLineHashtags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusContentProps: { statusContent },
|
||||||
|
hashtagsInBar: uniqueHashtagsWithCaseHandling(hashtagsInBar),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will process a status to, at the same time (avoiding parsing it twice):
|
||||||
|
* - build the HashtagBar for this status
|
||||||
|
* - remove the last-line hashtags from the status content
|
||||||
|
* @param status The status to process
|
||||||
|
* @returns Props to be passed to the <StatusContent> component, and the hashtagBar to render
|
||||||
|
*/
|
||||||
|
export function getHashtagBarForStatus(status: StatusLike) {
|
||||||
|
const { statusContentProps, hashtagsInBar } =
|
||||||
|
computeHashtagBarForStatus(status);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusContentProps,
|
||||||
|
hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const HashtagBar: React.FC<{
|
||||||
|
hashtags: string[];
|
||||||
|
}> = ({ hashtags }) => {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
setExpanded(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (hashtags.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revealedHashtags = expanded
|
||||||
|
? hashtags
|
||||||
|
: hashtags.slice(0, VISIBLE_HASHTAGS);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='hashtag-bar'>
|
||||||
|
{revealedHashtags.map((hashtag) => (
|
||||||
|
<Link key={hashtag} to={`/tags/${hashtag}`}>
|
||||||
|
#<span>{hashtag}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!expanded && hashtags.length > VISIBLE_HASHTAGS && (
|
||||||
|
<button className='link-button' onClick={handleClick}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='hashtags.and_other'
|
||||||
|
defaultMessage='…and {count, plural, other {# more}}'
|
||||||
|
values={{ count: hashtags.length - VISIBLE_HASHTAGS }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,3 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<HTMLImageElement> {
|
interface Props extends React.HTMLAttributes<HTMLImageElement> {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as React from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ interface States {
|
||||||
activate: boolean;
|
activate: boolean;
|
||||||
deactivate: boolean;
|
deactivate: boolean;
|
||||||
}
|
}
|
||||||
export class IconButton extends React.PureComponent<Props, States> {
|
export class IconButton extends PureComponent<Props, States> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
size: 18,
|
size: 18,
|
||||||
active: false,
|
active: false,
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
|
||||||
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);
|
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { cloneElement, Component } from 'react';
|
||||||
|
|
||||||
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
|
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
|
||||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
||||||
|
|
||||||
// Diff these props in the "unrendered" state
|
// Diff these props in the "unrendered" state
|
||||||
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
|
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
|
||||||
|
|
||||||
|
@ -38,7 +39,6 @@ export default class IntersectionObserverArticle extends Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { intersectionObserverWrapper, id } = this.props;
|
const { intersectionObserverWrapper, id } = this.props;
|
||||||
|
|
||||||
|
@ -106,24 +106,24 @@ export default class IntersectionObserverArticle extends Component {
|
||||||
const { children, id, index, listLength, cachedHeight } = this.props;
|
const { children, id, index, listLength, cachedHeight } = this.props;
|
||||||
const { isIntersecting, isHidden } = this.state;
|
const { isIntersecting, isHidden } = this.state;
|
||||||
|
|
||||||
const style = {};
|
|
||||||
|
|
||||||
if (!isIntersecting && (isHidden || cachedHeight)) {
|
if (!isIntersecting && (isHidden || cachedHeight)) {
|
||||||
style.height = `${this.height || cachedHeight || 150}px`;
|
|
||||||
style.opacity = 0;
|
|
||||||
style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
ref={this.handleRef}
|
ref={this.handleRef}
|
||||||
aria-posinset={index + 1}
|
aria-posinset={index + 1}
|
||||||
aria-setsize={listLength}
|
aria-setsize={listLength}
|
||||||
|
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
tabIndex={0}
|
tabIndex={-1}
|
||||||
style={style}
|
|
||||||
>
|
>
|
||||||
{children && cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })}
|
{children && cloneElement(children, { hidden: true })}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex={-1}>
|
||||||
|
{children && cloneElement(children, { hidden: false })}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { Blurhash } from 'flavours/glitch/components/blurhash';
|
import { Blurhash } from 'flavours/glitch/components/blurhash';
|
||||||
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
|
|
||||||
|
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
import { IconButton } from './icon_button';
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import 'wicg-inert';
|
import 'wicg-inert';
|
||||||
|
|
||||||
import { multiply } from 'color-blend';
|
import { multiply } from 'color-blend';
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
export default class ModalRoot extends PureComponent {
|
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
static contextTypes = {
|
class ModalRoot extends PureComponent {
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
@ -21,6 +20,7 @@ export default class ModalRoot extends PureComponent {
|
||||||
}),
|
}),
|
||||||
noEsc: PropTypes.bool,
|
noEsc: PropTypes.bool,
|
||||||
ignoreFocus: PropTypes.bool,
|
ignoreFocus: PropTypes.bool,
|
||||||
|
...WithOptionalRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
activeElement = this.props.children ? document.activeElement : null;
|
activeElement = this.props.children ? document.activeElement : null;
|
||||||
|
@ -56,7 +56,7 @@ export default class ModalRoot extends PureComponent {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
window.addEventListener('keyup', this.handleKeyUp, false);
|
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||||
window.addEventListener('keydown', this.handleKeyDown, false);
|
window.addEventListener('keydown', this.handleKeyDown, false);
|
||||||
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
|
this.history = this.props.history || createBrowserHistory();
|
||||||
|
|
||||||
if (this.props.children) {
|
if (this.props.children) {
|
||||||
this._handleModalOpen();
|
this._handleModalOpen();
|
||||||
|
@ -110,8 +110,9 @@ export default class ModalRoot extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleModalClose () {
|
_handleModalClose () {
|
||||||
|
if (this.unlistenHistory) {
|
||||||
this.unlistenHistory();
|
this.unlistenHistory();
|
||||||
|
}
|
||||||
const { state } = this.history.location;
|
const { state } = this.history.location;
|
||||||
if (state && state.mastodonModalKey === this._modalHistoryKey) {
|
if (state && state.mastodonModalKey === this._modalHistoryKey) {
|
||||||
this.history.goBack();
|
this.history.goBack();
|
||||||
|
@ -160,3 +161,5 @@ export default class ModalRoot extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withOptionalRouter(ModalRoot);
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import AccountNavigation from 'flavours/glitch/features/account/navigation';
|
|
||||||
import Trends from 'flavours/glitch/features/getting_started/containers/trends_container';
|
|
||||||
import { showTrends } from 'flavours/glitch/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 'flavours/glitch/features/account/navigation';
|
||||||
|
import Trends from 'flavours/glitch/features/getting_started/containers/trends_container';
|
||||||
|
import { showTrends } from 'flavours/glitch/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>
|
||||||
|
);
|
|
@ -1,5 +1,3 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export const NotSignedInIndicator: React.FC = () => (
|
export const NotSignedInIndicator: React.FC = () => (
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
export default class Permalink extends PureComponent {
|
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
static contextTypes = {
|
class Permalink extends PureComponent {
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
@ -13,6 +11,7 @@ export default class Permalink extends PureComponent {
|
||||||
to: PropTypes.string.isRequired,
|
to: PropTypes.string.isRequired,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
onInterceptClick: PropTypes.func,
|
onInterceptClick: PropTypes.func,
|
||||||
|
...WithOptionalRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = (e) => {
|
handleClick = (e) => {
|
||||||
|
@ -22,9 +21,9 @@ export default class Permalink extends PureComponent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.context.router) {
|
if (this.props.history) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.context.router.history.push(this.props.to);
|
this.props.history.push(this.props.to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -47,3 +46,5 @@ export default class Permalink extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withOptionalRouter(Permalink);
|
||||||
|
|
|
@ -16,7 +16,6 @@ import Motion from 'flavours/glitch/features/ui/util/optional_motion';
|
||||||
|
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
import { RelativeTimestamp } from './relative_timestamp';
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
closed: {
|
closed: {
|
||||||
id: 'poll.closed',
|
id: 'poll.closed',
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { createBrowserHistory } from 'history';
|
|
||||||
import { Router as OriginalRouter } from 'react-router';
|
import { Router as OriginalRouter } from 'react-router';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
LocationDescriptor,
|
||||||
|
LocationDescriptorObject,
|
||||||
|
Path,
|
||||||
|
} from 'history';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||||
|
|
||||||
interface MastodonLocationState {
|
interface MastodonLocationState {
|
||||||
fromMastodon?: boolean;
|
fromMastodon?: boolean;
|
||||||
mastodonModalKey?: string;
|
mastodonModalKey?: string;
|
||||||
}
|
}
|
||||||
|
type HistoryPath = Path | LocationDescriptor<MastodonLocationState>;
|
||||||
|
|
||||||
const browserHistory = createBrowserHistory<
|
const browserHistory = createBrowserHistory<
|
||||||
MastodonLocationState | undefined
|
MastodonLocationState | undefined
|
||||||
|
@ -17,28 +24,55 @@ const browserHistory = createBrowserHistory<
|
||||||
const originalPush = browserHistory.push.bind(browserHistory);
|
const originalPush = browserHistory.push.bind(browserHistory);
|
||||||
const originalReplace = browserHistory.replace.bind(browserHistory);
|
const originalReplace = browserHistory.replace.bind(browserHistory);
|
||||||
|
|
||||||
browserHistory.push = (path: string, state?: MastodonLocationState) => {
|
function normalizePath(
|
||||||
state = state ?? {};
|
path: HistoryPath,
|
||||||
state.fromMastodon = true;
|
state?: MastodonLocationState,
|
||||||
|
): LocationDescriptorObject<MastodonLocationState> {
|
||||||
|
const location = typeof path === 'string' ? { pathname: path } : { ...path };
|
||||||
|
|
||||||
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
if (location.state === undefined && state !== undefined) {
|
||||||
originalPush(`/deck${path}`, state);
|
location.state = state;
|
||||||
} else {
|
} else if (
|
||||||
originalPush(path, state);
|
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) {
|
if (browserHistory.location.state?.fromMastodon) {
|
||||||
state = state ?? {};
|
location.state = location.state ?? {};
|
||||||
state.fromMastodon = true;
|
location.state.fromMastodon = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
originalReplace(location);
|
||||||
originalReplace(`/deck${path}`, state);
|
|
||||||
} else {
|
|
||||||
originalReplace(path, state);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
|
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import { Children, cloneElement, PureComponent } from 'react';
|
import { Children, cloneElement, PureComponent } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -9,11 +10,11 @@ import { connect } from 'react-redux';
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
|
|
||||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||||
import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/intersection_observer_wrapper';
|
|
||||||
|
|
||||||
|
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
||||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
||||||
|
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
|
||||||
|
|
||||||
import { LoadMore } from './load_more';
|
import { LoadMore } from './load_more';
|
||||||
import { LoadPending } from './load_pending';
|
import { LoadPending } from './load_pending';
|
||||||
|
@ -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 = {
|
return (<IntersectionObserverArticleContainer
|
||||||
router: PropTypes.object,
|
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 = {
|
static propTypes = {
|
||||||
scrollKey: PropTypes.string.isRequired,
|
scrollKey: PropTypes.string.isRequired,
|
||||||
|
@ -331,13 +353,14 @@ class ScrollableList extends PureComponent {
|
||||||
{loadPending}
|
{loadPending}
|
||||||
|
|
||||||
{Children.map(this.props.children, (child, index) => (
|
{Children.map(this.props.children, (child, index) => (
|
||||||
<IntersectionObserverArticleContainer
|
<IOArticleContainerWrapper
|
||||||
key={child.key}
|
key={child.key}
|
||||||
id={child.key}
|
id={child.key}
|
||||||
index={index}
|
index={index}
|
||||||
listLength={childrenCount}
|
listLength={childrenCount}
|
||||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||||
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
|
trackScroll={trackScroll}
|
||||||
|
scrollKey={scrollKey}
|
||||||
>
|
>
|
||||||
{cloneElement(child, {
|
{cloneElement(child, {
|
||||||
getScrollPosition: this.getScrollPosition,
|
getScrollPosition: this.getScrollPosition,
|
||||||
|
@ -345,7 +368,7 @@ class ScrollableList extends PureComponent {
|
||||||
cachedMediaWidth: this.state.cachedMediaWidth,
|
cachedMediaWidth: this.state.cachedMediaWidth,
|
||||||
cacheMediaWidth: this.cacheMediaWidth,
|
cacheMediaWidth: this.cacheMediaWidth,
|
||||||
})}
|
})}
|
||||||
</IntersectionObserverArticleContainer>
|
</IOArticleContainerWrapper>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{loadMore}
|
{loadMore}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
|
|
|
@ -12,14 +12,18 @@ import { HotKeys } from 'react-hotkeys';
|
||||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
||||||
import PollContainer from 'flavours/glitch/containers/poll_container';
|
import PollContainer from 'flavours/glitch/containers/poll_container';
|
||||||
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
|
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
|
||||||
import { displayMedia } from 'flavours/glitch/initial_state';
|
|
||||||
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
|
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
|
||||||
|
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
import Card from '../features/status/components/card';
|
import Card from '../features/status/components/card';
|
||||||
|
// We use the component (and not the container) since we do not want
|
||||||
|
// to use the progress bar to show download progress
|
||||||
import Bundle from '../features/ui/components/bundle';
|
import Bundle from '../features/ui/components/bundle';
|
||||||
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||||
|
import { displayMedia } from '../initial_state';
|
||||||
|
|
||||||
import AttachmentList from './attachment_list';
|
import AttachmentList from './attachment_list';
|
||||||
|
import { getHashtagBarForStatus } from './hashtag_bar';
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import StatusHeader from './status_header';
|
import StatusHeader from './status_header';
|
||||||
|
@ -67,10 +71,6 @@ export const defaultMediaVisibility = (status, settings) => {
|
||||||
|
|
||||||
class Status extends ImmutablePureComponent {
|
class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
containerId: PropTypes.string,
|
containerId: PropTypes.string,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
|
@ -118,6 +118,7 @@ class Status extends ImmutablePureComponent {
|
||||||
inUse: PropTypes.bool,
|
inUse: PropTypes.bool,
|
||||||
available: PropTypes.bool,
|
available: PropTypes.bool,
|
||||||
}),
|
}),
|
||||||
|
...WithOptionalRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -357,10 +358,9 @@ class Status extends ImmutablePureComponent {
|
||||||
// Otherwise, we open the url handed to us in `destination`, if
|
// Otherwise, we open the url handed to us in `destination`, if
|
||||||
// applicable.
|
// applicable.
|
||||||
parseClick = (e, destination) => {
|
parseClick = (e, destination) => {
|
||||||
const { router } = this.context;
|
const { status, history } = this.props;
|
||||||
const { status } = this.props;
|
|
||||||
const { isCollapsed } = this.state;
|
const { isCollapsed } = this.state;
|
||||||
if (!router) return;
|
if (!history) return;
|
||||||
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
|
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
|
||||||
if (isCollapsed) this.setCollapsed(false);
|
if (isCollapsed) this.setCollapsed(false);
|
||||||
|
@ -378,7 +378,7 @@ class Status extends ImmutablePureComponent {
|
||||||
status.getIn(['reblog', 'id'], status.get('id'))
|
status.getIn(['reblog', 'id'], status.get('id'))
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
router.history.push(destination);
|
history.push(destination);
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -432,7 +432,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleHotkeyReply = e => {
|
handleHotkeyReply = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onReply(this.props.status, this.context.router.history);
|
this.props.onReply(this.props.status, this.props.history);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyFavourite = (e) => {
|
handleHotkeyFavourite = (e) => {
|
||||||
|
@ -449,16 +449,16 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleHotkeyMention = e => {
|
handleHotkeyMention = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
this.props.onMention(this.props.status.get('account'), this.props.history);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyOpen = () => {
|
handleHotkeyOpen = () => {
|
||||||
const status = this.props.status;
|
const status = this.props.status;
|
||||||
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyOpenProfile = () => {
|
handleHotkeyOpenProfile = () => {
|
||||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyMoveUp = e => {
|
handleHotkeyMoveUp = e => {
|
||||||
|
@ -515,7 +515,6 @@ class Status extends ImmutablePureComponent {
|
||||||
parseClick,
|
parseClick,
|
||||||
setCollapsed,
|
setCollapsed,
|
||||||
} = this;
|
} = this;
|
||||||
const { router } = this.context;
|
|
||||||
const {
|
const {
|
||||||
intl,
|
intl,
|
||||||
status,
|
status,
|
||||||
|
@ -534,6 +533,7 @@ class Status extends ImmutablePureComponent {
|
||||||
previousId,
|
previousId,
|
||||||
nextInReplyToId,
|
nextInReplyToId,
|
||||||
rootId,
|
rootId,
|
||||||
|
history,
|
||||||
...other
|
...other
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { isCollapsed } = this.state;
|
const { isCollapsed } = this.state;
|
||||||
|
@ -783,6 +783,9 @@ class Status extends ImmutablePureComponent {
|
||||||
muted,
|
muted,
|
||||||
}, 'focusable');
|
}, 'focusable');
|
||||||
|
|
||||||
|
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||||
|
contentMedia.push(hashtagBar);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
<div
|
<div
|
||||||
|
@ -829,9 +832,10 @@ class Status extends ImmutablePureComponent {
|
||||||
onExpandedToggle={this.handleExpandedToggle}
|
onExpandedToggle={this.handleExpandedToggle}
|
||||||
onTranslate={this.handleTranslate}
|
onTranslate={this.handleTranslate}
|
||||||
parseClick={parseClick}
|
parseClick={parseClick}
|
||||||
disabled={!router}
|
disabled={!history}
|
||||||
tagLinks={settings.get('tag_misleading_links')}
|
tagLinks={settings.get('tag_misleading_links')}
|
||||||
rewriteMentions={settings.get('rewrite_mentions')}
|
rewriteMentions={settings.get('rewrite_mentions')}
|
||||||
|
{...statusContentProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
|
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
|
||||||
|
@ -855,4 +859,4 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(Status);
|
export default withOptionalRouter(injectIntl(Status));
|
||||||
|
|
|
@ -3,14 +3,17 @@ import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
|
||||||
import { me } from 'flavours/glitch/initial_state';
|
|
||||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
|
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
|
||||||
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
|
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
|
||||||
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
|
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
||||||
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
import { IconButton } from './icon_button';
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
import { RelativeTimestamp } from './relative_timestamp';
|
||||||
|
@ -54,7 +57,6 @@ const messages = defineMessages({
|
||||||
class StatusActionBar extends ImmutablePureComponent {
|
class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
|
||||||
identity: PropTypes.object,
|
identity: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,6 +84,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
showReplyCount: PropTypes.bool,
|
showReplyCount: PropTypes.bool,
|
||||||
scrollKey: PropTypes.string,
|
scrollKey: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
...WithRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
|
@ -97,12 +100,23 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
this.props.onReply(this.props.status, this.context.router.history);
|
this.props.onReply(this.props.status, this.props.history);
|
||||||
} else {
|
} else {
|
||||||
this.props.onInteractionModal('reply', this.props.status);
|
this.props.onInteractionModal('reply', this.props.status);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleQuoteClick = () => {
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
|
this.props.onQuote(this.props.status, this.props.history);
|
||||||
|
} else {
|
||||||
|
// TODO(ariadne): Add an interaction modal for quoting specifically.
|
||||||
|
this.props.onInteractionModal('reply', this.props.status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleShareClick = () => {
|
handleShareClick = () => {
|
||||||
navigator.share({
|
navigator.share({
|
||||||
url: this.props.status.get('url'),
|
url: this.props.status.get('url'),
|
||||||
|
@ -129,31 +143,20 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleQuoteClick = () => {
|
|
||||||
const { signedIn } = this.context.identity;
|
|
||||||
|
|
||||||
if (signedIn) {
|
|
||||||
this.props.onQuote(this.props.status, this.context.router.history);
|
|
||||||
} else {
|
|
||||||
// TODO(ariadne): Add an interaction modal for quoting specifically.
|
|
||||||
this.props.onInteractionModal('reply', this.props.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBookmarkClick = (e) => {
|
handleBookmarkClick = (e) => {
|
||||||
this.props.onBookmark(this.props.status, e);
|
this.props.onBookmark(this.props.status, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history);
|
this.props.onDelete(this.props.status, this.props.history);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRedraftClick = () => {
|
handleRedraftClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onDelete(this.props.status, this.props.history, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEditClick = () => {
|
handleEditClick = () => {
|
||||||
this.props.onEdit(this.props.status, this.context.router.history);
|
this.props.onEdit(this.props.status, this.props.history);
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePinClick = () => {
|
handlePinClick = () => {
|
||||||
|
@ -161,11 +164,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMentionClick = () => {
|
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 = () => {
|
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 = () => {
|
handleMuteClick = () => {
|
||||||
|
@ -177,12 +180,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOpen = () => {
|
handleOpen = () => {
|
||||||
let state = { ...this.context.router.history.location.state };
|
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
|
||||||
if (state.mastodonModalKey) {
|
|
||||||
this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
|
|
||||||
} else {
|
|
||||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEmbed = () => {
|
handleEmbed = () => {
|
||||||
|
@ -355,4 +353,4 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(StatusActionBar);
|
export default withRouter(injectIntl(StatusActionBar));
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { PureComponent } from 'react';
|
||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -68,6 +69,15 @@ const isLinkMisleading = (link) => {
|
||||||
return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host));
|
return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {any} status
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getStatusContent(status) {
|
||||||
|
return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
|
||||||
|
}
|
||||||
|
|
||||||
class TranslateButton extends PureComponent {
|
class TranslateButton extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -117,6 +127,7 @@ class StatusContent extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
|
statusContent: PropTypes.string,
|
||||||
expanded: PropTypes.bool,
|
expanded: PropTypes.bool,
|
||||||
collapsed: PropTypes.bool,
|
collapsed: PropTypes.bool,
|
||||||
onExpandedToggle: PropTypes.func,
|
onExpandedToggle: PropTypes.func,
|
||||||
|
@ -131,6 +142,10 @@ class StatusContent extends PureComponent {
|
||||||
rewriteMentions: PropTypes.string,
|
rewriteMentions: PropTypes.string,
|
||||||
languages: ImmutablePropTypes.map,
|
languages: ImmutablePropTypes.map,
|
||||||
intl: PropTypes.object,
|
intl: PropTypes.object,
|
||||||
|
// from react-router
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
location: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -322,6 +337,7 @@ class StatusContent extends PureComponent {
|
||||||
tagLinks,
|
tagLinks,
|
||||||
rewriteMentions,
|
rewriteMentions,
|
||||||
intl,
|
intl,
|
||||||
|
statusContent,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||||
|
@ -329,7 +345,7 @@ class StatusContent extends PureComponent {
|
||||||
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
||||||
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
||||||
|
|
||||||
const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
|
const content = { __html: statusContent ?? getStatusContent(status) };
|
||||||
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
||||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
const classNames = classnames('status__content', {
|
const classNames = classnames('status__content', {
|
||||||
|
@ -506,4 +522,4 @@ class StatusContent extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(injectIntl(StatusContent));
|
export default withRouter(connect(mapStateToProps)(injectIntl(StatusContent)));
|
||||||
|
|
|
@ -6,7 +6,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import RegenerationIndicator from 'flavours/glitch/components/regeneration_indicator';
|
import RegenerationIndicator from 'flavours/glitch/components/regeneration_indicator';
|
||||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
|
||||||
|
import StatusContainer from '../containers/status_container';
|
||||||
|
|
||||||
import { LoadGap } from './load_gap';
|
import { LoadGap } from './load_gap';
|
||||||
import ScrollableList from './scrollable_list';
|
import ScrollableList from './scrollable_list';
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -9,12 +9,12 @@ import {
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
muteAccount,
|
muteAccount,
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
} from 'flavours/glitch/actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from '../actions/mutes';
|
||||||
import Account from 'flavours/glitch/components/account';
|
import Account from '../components/account';
|
||||||
import { unfollowModal } from 'flavours/glitch/initial_state';
|
import { unfollowModal } from '../initial_state';
|
||||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
import { makeGetAccount } from '../selectors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
|
|
|
@ -2,12 +2,13 @@ import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||||
import { hydrateStore } from 'flavours/glitch/actions/store';
|
import { hydrateStore } from '../actions/store';
|
||||||
import Compose from 'flavours/glitch/features/standalone/compose';
|
import Compose from '../features/standalone/compose';
|
||||||
import initialState from 'flavours/glitch/initial_state';
|
import initialState from '../initial_state';
|
||||||
import { IntlProvider } from 'flavours/glitch/locales';
|
import { IntlProvider } from '../locales';
|
||||||
import { store } from 'flavours/glitch/store';
|
import { store } from '../store';
|
||||||
|
|
||||||
|
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
store.dispatch(hydrateStore(initialState));
|
store.dispatch(hydrateStore(initialState));
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu';
|
import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
|
||||||
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
|
import { openModal, closeModal } from '../actions/modal';
|
||||||
import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
import DropdownMenu from '../components/dropdown_menu';
|
||||||
|
|
||||||
import { isUserTouching } from '../is_mobile';
|
import { isUserTouching } from '../is_mobile';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { setHeight } from 'flavours/glitch/actions/height_cache';
|
import { setHeight } from '../actions/height_cache';
|
||||||
import IntersectionObserverArticle from 'flavours/glitch/components/intersection_observer_article';
|
import IntersectionObserverArticle from '../components/intersection_observer_article';
|
||||||
|
|
||||||
const makeMapStateToProps = (state, props) => ({
|
const makeMapStateToProps = (state, props) => ({
|
||||||
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
|
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue