Browse Source

Merge pull request #1151 from garbados/hometown-dev

pull/907/merge v1.0.5+3.4.6
Darius Kazemi 4 years ago committed by GitHub
parent
commit
ee513a6a3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 63
      .circleci/config.yml
  2. 4
      .dockerignore
  3. 6
      .env.production.sample
  4. 2
      .github/CODEOWNERS
  5. 34
      .github/workflows/build-image.yml
  6. 34
      .github/workflows/check-i18n.yml
  7. 4
      AUTHORS.md
  8. 2734
      CHANGELOG.md
  9. 4
      CONTRIBUTING.md
  10. 4
      Dockerfile
  11. 14
      Gemfile
  12. 49
      Gemfile.lock
  13. 4
      app.json
  14. 4
      app/controllers/activitypub/followers_synchronizations_controller.rb
  15. 16
      app/controllers/activitypub/outboxes_controller.rb
  16. 2
      app/controllers/admin/statuses_controller.rb
  17. 11
      app/controllers/api/v1/emails/confirmations_controller.rb
  18. 1
      app/controllers/auth/passwords_controller.rb
  19. 3
      app/controllers/auth/registrations_controller.rb
  20. 21
      app/controllers/auth/sessions_controller.rb
  21. 21
      app/controllers/concerns/sign_in_token_authentication_concern.rb
  22. 24
      app/controllers/concerns/two_factor_authentication_concern.rb
  23. 5
      app/controllers/custom_css_controller.rb
  24. 2
      app/controllers/follower_accounts_controller.rb
  25. 2
      app/controllers/following_accounts_controller.rb
  26. 2
      app/controllers/media_proxy_controller.rb
  27. 2
      app/controllers/settings/deletes_controller.rb
  28. 3
      app/controllers/well_known/webfinger_controller.rb
  29. 6
      app/helpers/accounts_helper.rb
  30. 11
      app/helpers/application_helper.rb
  31. 55
      app/helpers/context_helper.rb
  32. 80
      app/helpers/jsonld_helper.rb
  33. 1
      app/helpers/settings_helper.rb
  34. 21
      app/javascript/mastodon/actions/picture_in_picture.js
  35. 2
      app/javascript/mastodon/components/intersection_observer_article.js
  36. 40
      app/javascript/mastodon/components/modal_root.js
  37. 7
      app/javascript/mastodon/components/scrollable_list.js
  38. 4
      app/javascript/mastodon/components/status.js
  39. 5
      app/javascript/mastodon/components/status_list.js
  40. 6
      app/javascript/mastodon/containers/mastodon.js
  41. 18
      app/javascript/mastodon/containers/scroll_container.js
  42. 7
      app/javascript/mastodon/features/account_gallery/index.js
  43. 4
      app/javascript/mastodon/features/account_timeline/index.js
  44. 4
      app/javascript/mastodon/features/blocks/index.js
  45. 4
      app/javascript/mastodon/features/bookmarked_statuses/index.js
  46. 4
      app/javascript/mastodon/features/community_timeline/index.js
  47. 25
      app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
  48. 1
      app/javascript/mastodon/features/compose/containers/navigation_container.js
  49. 1
      app/javascript/mastodon/features/compose/index.js
  50. 1
      app/javascript/mastodon/features/direct_timeline/components/conversations_list.js
  51. 4
      app/javascript/mastodon/features/direct_timeline/index.js
  52. 7
      app/javascript/mastodon/features/directory/index.js
  53. 4
      app/javascript/mastodon/features/domain_blocks/index.js
  54. 33
      app/javascript/mastodon/features/emoji/emoji_compressed.js
  55. 4
      app/javascript/mastodon/features/emoji/unicode_to_unified_name.js
  56. 4
      app/javascript/mastodon/features/favourited_statuses/index.js
  57. 4
      app/javascript/mastodon/features/favourites/index.js
  58. 4
      app/javascript/mastodon/features/follow_requests/index.js
  59. 4
      app/javascript/mastodon/features/followers/index.js
  60. 4
      app/javascript/mastodon/features/following/index.js
  61. 4
      app/javascript/mastodon/features/hashtag_timeline/index.js
  62. 4
      app/javascript/mastodon/features/home_timeline/index.js
  63. 4
      app/javascript/mastodon/features/list_timeline/index.js
  64. 3
      app/javascript/mastodon/features/lists/index.js
  65. 4
      app/javascript/mastodon/features/mutes/index.js
  66. 4
      app/javascript/mastodon/features/notifications/index.js
  67. 6
      app/javascript/mastodon/features/picture_in_picture/components/footer.js
  68. 4
      app/javascript/mastodon/features/pinned_statuses/index.js
  69. 4
      app/javascript/mastodon/features/public_timeline/index.js
  70. 4
      app/javascript/mastodon/features/reblogs/index.js
  71. 12
      app/javascript/mastodon/features/status/index.js
  72. 27
      app/javascript/mastodon/features/ui/components/audio_modal.js
  73. 9
      app/javascript/mastodon/features/ui/components/confirmation_modal.js
  74. 1
      app/javascript/mastodon/features/ui/components/link_footer.js
  75. 24
      app/javascript/mastodon/features/ui/components/media_modal.js
  76. 24
      app/javascript/mastodon/features/ui/components/video_modal.js
  77. 4
      app/javascript/mastodon/features/ui/containers/modal_container.js
  78. 60
      app/javascript/mastodon/features/ui/index.js
  79. 2
      app/javascript/mastodon/locales/af.json
  80. 2
      app/javascript/mastodon/locales/cs.json
  81. 8
      app/javascript/mastodon/locales/defaultMessages.json
  82. 2
      app/javascript/mastodon/locales/en.json
  83. 2
      app/javascript/mastodon/locales/es-AR.json
  84. 28
      app/javascript/mastodon/locales/es-MX.json
  85. 18
      app/javascript/mastodon/locales/es.json
  86. 74
      app/javascript/mastodon/locales/gd.json
  87. 2
      app/javascript/mastodon/locales/hu.json
  88. 2
      app/javascript/mastodon/locales/io.json
  89. 2
      app/javascript/mastodon/locales/it.json
  90. 2
      app/javascript/mastodon/locales/kn.json
  91. 4
      app/javascript/mastodon/locales/ko.json
  92. 36
      app/javascript/mastodon/locales/lt.json
  93. 2
      app/javascript/mastodon/locales/mk.json
  94. 12
      app/javascript/mastodon/locales/ml.json
  95. 2
      app/javascript/mastodon/locales/mr.json
  96. 6
      app/javascript/mastodon/locales/nl.json
  97. 2
      app/javascript/mastodon/locales/pa.json
  98. 4
      app/javascript/mastodon/locales/pt-PT.json
  99. 2
      app/javascript/mastodon/locales/si.json
  100. 12
      app/javascript/mastodon/locales/sv.json
  101. Some files were not shown because too many files have changed in this diff Show More

63
.circleci/config.yml

@ -167,8 +167,45 @@ jobs:
name: Create database
command: ./bin/rails db:create
- run:
name: Run migrations
command: ./bin/rails db:migrate VERSION=20171010025614
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all remaining migrations
test-two-step-migrations:
<<: *defaults
docker:
- image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
steps:
- *attach_workspace
- *install_system_dependencies
- run:
command: ./bin/rails db:create
name: Create database
- run:
command: ./bin/rails db:migrate VERSION=20171010025614
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all pre-deployment migrations
evironment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails db:migrate
name: Run all post-deployment remaining migrations
test-ruby2.7:
<<: *defaults
@ -216,24 +253,6 @@ jobs:
name: Run jest
command: yarn test:jest
check-i18n:
<<: *defaults
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Check locale file normalization
command: bundle exec i18n-tasks check-normalized
- run:
name: Check for unused strings
command: bundle exec i18n-tasks unused -l en
- run:
name: Check for wrong string interpolations
command: bundle exec i18n-tasks check-consistent-interpolations
- run:
name: Check that all required locale files exist
command: bundle exec rake repo:check_locales_files
workflows:
version: 2
build-and-test:
@ -256,6 +275,9 @@ workflows:
- test-migrations:
requires:
- install-ruby2.7
- test-two-step-migrations:
requires:
- install-ruby2.7
- test-ruby2.7:
requires:
- install-ruby2.7
@ -271,6 +293,3 @@ workflows:
- test-webui:
requires:
- install
- check-i18n:
requires:
- install-ruby2.7

4
.dockerignore

@ -1,6 +1,10 @@
.bundle
.env
.env.*
.git
.gitattributes
.gitignore
.github
public/system
public/assets
public/packs

6
.env.production.sample

@ -4,6 +4,12 @@
# not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
# Note that this file accepts slightly different syntax depending on whether
# you are using `docker-compose` or not. In particular, if you use
# `docker-compose`, the value of each declared variable will be taken verbatim,
# including surrounding quotes.
# See: https://github.com/mastodon/mastodon/issues/16895
# Federation
# ----------
# This identifies your server and cannot be changed safely later

2
.github/CODEOWNERS

@ -1,4 +1,4 @@
# CODEOWNERS for tootsuite/mastodon
# CODEOWNERS for mastodon/mastodon
# Translators
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.

34
.github/workflows/build-image.yml

@ -0,0 +1,34 @@
name: Build container image
on:
workflow_dispatch:
push:
branches:
- "main"
tags:
- "*"
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/metadata-action@v3
id: meta
with:
images: tootsuite/mastodon
flavor: |
latest=auto
tags: |
type=edge,branch=main
type=semver,pattern={{ raw }}
- uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=tootsuite/mastodon:latest
cache-to: type=inline

34
.github/workflows/check-i18n.yml

@ -0,0 +1,34 @@
name: Check i18n
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
RAILS_ENV: test
jobs:
check-i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '2.7'
bundler-cache: true
- name: Check locale file normalization
run: bundle exec i18n-tasks check-normalized
- name: Check for unused strings
run: bundle exec i18n-tasks unused -l en
- name: Check for wrong string interpolations
run: bundle exec i18n-tasks check-consistent-interpolations
- name: Check that all required locale files exist
run: bundle exec rake repo:check_locales_files

4
AUTHORS.md

@ -1,7 +1,7 @@
Authors
=======
Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon)
Mastodon is available on [GitHub](https://github.com/mastodon/mastodon)
and provided thanks to the work of the following contributors:
* [Gargron](https://github.com/Gargron)
@ -719,7 +719,7 @@ and provided thanks to the work of the following contributors:
* [西小倉宏信](mailto:nishiko@mindia.jp)
* [雨宮美羽](mailto:k737566@gmail.com)
This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead.
This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/mastodon/mastodon/graphs/contributors) instead.
## Translators

2734
CHANGELOG.md

File diff suppressed because it is too large Load Diff

4
CONTRIBUTING.md

@ -14,7 +14,7 @@ If your contributions are accepted into Mastodon, you can request to be paid thr
## Bug reports
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
## Translations
@ -44,4 +44,4 @@ It is not always possible to phrase every change in such a manner, but it is des
## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to tootsuite/documentation](https://github.com/tootsuite/documentation).
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).

4
Dockerfile

@ -54,8 +54,8 @@ RUN npm install -g yarn && \
COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /opt/mastodon && \
bundle config set deployment 'true' && \
bundle config set without 'development test' && \
bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install -j"$(nproc)" && \
yarn install --pure-lockfile

14
Gemfile

@ -17,7 +17,7 @@ gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.94', require: false
gem 'aws-sdk-s3', '~> 1.95', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@ -55,7 +55,7 @@ gem 'redis-namespace', '~> 1.8'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 4.4'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.4.3'
gem 'httplog', '~> 1.5.0'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
@ -83,7 +83,7 @@ gem 'sidekiq', '~> 6.2'
gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 7.0'
gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.1'
gem 'simple-navigation', '~> 4.3'
gem 'simple_form', '~> 5.1'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.1'
@ -91,7 +91,7 @@ gem 'strong_migrations', '~> 0.7'
gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2021'
gem 'webpacker', '~> 5.3'
gem 'webpacker', '~> 5.4'
gem 'webpush', '~> 0.3'
gem 'webauthn', '~> 3.0.0.alpha1'
@ -115,12 +115,12 @@ end
group :test do
gem 'capybara', '~> 3.35'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.17'
gem 'faker', '~> 2.18'
gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.21', require: false
gem 'webmock', '~> 3.12'
gem 'webmock', '~> 3.13'
gem 'parallel_tests', '~> 3.7'
gem 'rspec_junit_formatter', '~> 0.4'
end
@ -134,7 +134,7 @@ group :development do
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler'
gem 'rubocop', '~> 1.14', require: false
gem 'rubocop', '~> 1.15', require: false
gem 'rubocop-rails', '~> 2.10', require: false
gem 'brakeman', '~> 5.0', require: false
gem 'bundler-audit', '~> 0.8', require: false

49
Gemfile.lock

@ -79,7 +79,7 @@ GEM
encryptor (~> 3.0.0)
awrence (1.1.1)
aws-eventstream (1.1.1)
aws-partitions (1.452.0)
aws-partitions (1.465.0)
aws-sdk-core (3.114.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
@ -88,7 +88,7 @@ GEM
aws-sdk-kms (1.43.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.94.1)
aws-sdk-s3 (1.95.1)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
@ -211,7 +211,7 @@ GEM
tzinfo
excon (0.76.0)
fabrication (2.22.0)
faker (2.17.0)
faker (2.18.0)
i18n (>= 1.6, < 2)
faraday (1.3.0)
faraday-net_http (~> 1.0)
@ -273,7 +273,7 @@ GEM
http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0)
http_accept_language (2.1.1)
httplog (1.4.3)
httplog (1.5.0)
rack (>= 1.0)
rainbow (>= 2.0.0)
i18n (1.8.10)
@ -354,17 +354,22 @@ GEM
nokogiri (~> 1)
rake
mini_mime (1.0.3)
mini_portile2 (2.5.1)
mini_portile2 (2.5.2)
net-ftp (~> 0.1)
minitest (5.14.4)
msgpack (1.4.2)
multi_json (1.15.0)
multipart-post (2.1.1)
net-ftp (0.1.2)
net-protocol
time
net-ldap (0.17.0)
net-protocol (0.1.0)
net-scp (3.0.0)
net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.1.0)
nio4r (2.5.7)
nokogiri (1.11.3)
nokogiri (1.11.6)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogumbo (2.0.4)
@ -428,7 +433,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
puma (5.3.0)
puma (5.3.2)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
@ -524,7 +529,7 @@ GEM
rspec-support (3.10.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.14.0)
rubocop (1.15.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
@ -540,8 +545,9 @@ GEM
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
ruby-progressbar (1.11.0)
ruby-saml (1.11.0)
nokogiri (>= 1.5.10)
ruby-saml (1.13.0)
nokogiri (>= 1.10.5)
rexml
ruby2_keywords (0.0.4)
rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6)
@ -569,12 +575,12 @@ GEM
sidekiq (>= 3)
thwait
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.0.9)
sidekiq-unique-jobs (7.0.11)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 7.0)
thor (>= 0.20, < 2.0)
simple-navigation (4.1.0)
simple-navigation (4.3.0)
activesupport (>= 2.3.2)
simple_form (5.1.0)
actionpack (>= 5.2)
@ -609,6 +615,7 @@ GEM
thwait (0.2.0)
e2mmap
tilt (2.0.10)
time (0.1.0)
tpm-key_attestation (0.9.0)
bindata (~> 2.4)
openssl-signature_algorithm (~> 0.4.0)
@ -646,11 +653,11 @@ GEM
safety_net_attestation (~> 0.4.0)
securecompare (~> 1.0)
tpm-key_attestation (~> 0.9.0)
webmock (3.12.2)
webmock (3.13.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.3.0)
webpacker (5.4.0)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
@ -675,7 +682,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.8)
addressable (~> 2.7)
annotate (~> 3.1)
aws-sdk-s3 (~> 1.94)
aws-sdk-s3 (~> 1.95)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
@ -704,7 +711,7 @@ DEPENDENCIES
dotenv-rails (~> 2.7)
ed25519 (~> 1.2)
fabrication (~> 2.22)
faker (~> 2.17)
faker (~> 2.18)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
@ -715,7 +722,7 @@ DEPENDENCIES
htmlentities (~> 4.3)
http (~> 4.4)
http_accept_language (~> 2.1)
httplog (~> 1.4.3)
httplog (~> 1.5.0)
i18n-tasks (~> 0.9)
idn-ruby
iso-639
@ -769,7 +776,7 @@ DEPENDENCIES
rspec-rails (~> 5.0)
rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4)
rubocop (~> 1.14)
rubocop (~> 1.15)
rubocop-rails (~> 2.10)
ruby-progressbar (~> 1.11)
sanitize (~> 5.2)
@ -778,7 +785,7 @@ DEPENDENCIES
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0)
sidekiq-unique-jobs (~> 7.0)
simple-navigation (~> 4.1)
simple-navigation (~> 4.3)
simple_form (~> 5.1)
simplecov (~> 0.21)
sprockets (~> 3.7.2)
@ -791,8 +798,8 @@ DEPENDENCIES
twitter-text (~> 3.1.0)
tzinfo-data (~> 1.2021)
webauthn (~> 3.0.0.alpha1)
webmock (~> 3.12)
webpacker (~> 5.3)
webmock (~> 3.13)
webpacker (~> 5.4)
webpush (~> 0.3)
xorcist (~> 1.1)

4
app.json

@ -1,8 +1,8 @@
{
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/tootsuite.png",
"repository": "https://github.com/mastodon/mastodon",
"logo": "https://github.com/mastodon.png",
"env": {
"HEROKU": {
"description": "Leave this as true",

4
app/controllers/activitypub/followers_synchronizations_controller.rb

@ -19,11 +19,11 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
private
def uri_prefix
signed_request_account.uri[/http(s?):\/\/[^\/]+\//]
signed_request_account.uri[Account::URL_PREFIX_RE]
end
def set_items
@items = @account.followers.where(Account.arel_table[:uri].matches(uri_prefix + '%', false, true)).pluck(:uri)
@items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%", false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri)
end
def collection_presenter

16
app/controllers/activitypub/outboxes_controller.rb

@ -11,7 +11,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
before_action :set_cache_headers
def show
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
if page_requested?
expires_in(1.minute, public: public_fetch_mode? && signed_request_account.nil?)
else
expires_in(3.minutes, public: public_fetch_mode?)
end
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
@ -29,7 +33,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
)
else
ActivityPub::CollectionPresenter.new(
id: account_outbox_url(@account),
id: outbox_url,
type: :ordered,
size: @account.statuses_count,
first: outbox_url(page: true),
@ -47,11 +51,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
end
def next_page
account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
outbox_url(page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
end
def prev_page
account_outbox_url(@account, page: true, min_id: @statuses.first.id) unless @statuses.empty?
outbox_url(page: true, min_id: @statuses.first.id) unless @statuses.empty?
end
def set_statuses
@ -76,4 +80,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
end
end

2
app/controllers/admin/statuses_controller.rb

@ -14,7 +14,7 @@ module Admin
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media]
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id))
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)).reorder('statuses.id desc')
end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)

11
app/controllers/api/v1/emails/confirmations_controller.rb

@ -3,12 +3,11 @@
class Api::V1::Emails::ConfirmationsController < Api::BaseController
before_action :doorkeeper_authorize!
before_action :require_user_owned_by_application!
before_action :require_user_not_confirmed!
def create
if !current_user.confirmed? && current_user.unconfirmed_email.present?
current_user.update!(email: params[:email]) if params.key?(:email)
current_user.resend_confirmation_instructions
end
current_user.update!(email: params[:email]) if params.key?(:email)
current_user.resend_confirmation_instructions
render_empty
end
@ -18,4 +17,8 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController
def require_user_owned_by_application!
render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: :forbidden unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id
end
def require_user_not_confirmed!
render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: :forbidden if current_user.confirmed? || current_user.unconfirmed_email.blank?
end
end

1
app/controllers/auth/passwords_controller.rb

@ -10,7 +10,6 @@ class Auth::PasswordsController < Devise::PasswordsController
super do |resource|
if resource.errors.empty?
resource.session_activations.destroy_all
resource.forget_me!
end
end
end

3
app/controllers/auth/registrations_controller.rb

@ -1,7 +1,6 @@
# frozen_string_literal: true
class Auth::RegistrationsController < Devise::RegistrationsController
include Devise::Controllers::Rememberable
include RegistrationSpamConcern
layout :determine_layout
@ -30,8 +29,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
super do |resource|
if resource.saved_change_to_encrypted_password?
resource.clear_other_sessions(current_session.session_id)
resource.forget_me!
remember_me(resource)
end
end
end

21
app/controllers/auth/sessions_controller.rb

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Auth::SessionsController < Devise::SessionsController
include Devise::Controllers::Rememberable
layout 'auth'
skip_before_action :require_no_authentication, only: [:create]
@ -26,7 +24,6 @@ class Auth::SessionsController < Devise::SessionsController
def create
super do |resource|
resource.update_sign_in!(request, new_sign_in: true)
remember_me(resource)
flash.delete(:notice)
end
end
@ -40,7 +37,7 @@ class Auth::SessionsController < Devise::SessionsController
end
def webauthn_options
user = find_user
user = User.find_by(id: session[:attempt_user_id])
if user.webauthn_enabled?
options_for_get = WebAuthn::Credential.options_for_get(
@ -58,16 +55,20 @@ class Auth::SessionsController < Devise::SessionsController
protected
def find_user
if session[:attempt_user_id]
if user_params[:email].present?
find_user_from_params
elsif session[:attempt_user_id]
User.find_by(id: session[:attempt_user_id])
else
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
user
end
end
def find_user_from_params
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
user
end
def user_params
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
end

21
app/controllers/concerns/sign_in_token_authentication_concern.rb

@ -16,21 +16,24 @@ module SignInTokenAuthenticationConcern
end
def authenticate_with_sign_in_token
user = self.resource = find_user
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user_params.key?(:sign_in_token_attempt) && session[:attempt_user_id]
authenticate_with_sign_in_token_attempt(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_sign_in_token(user)
if user_params[:email].present?
user = self.resource = find_user_from_params
prompt_for_sign_in_token(user) if user&.external_or_valid_password?(user_params[:password])
elsif session[:attempt_user_id]
user = self.resource = User.find_by(id: session[:attempt_user_id])
return if user.nil?
if session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user_params.key?(:sign_in_token_attempt)
authenticate_with_sign_in_token_attempt(user)
end
end
end
def authenticate_with_sign_in_token_attempt(user)
if valid_sign_in_token_attempt?(user)
clear_attempt_from_session
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')

24
app/controllers/concerns/two_factor_authentication_concern.rb

@ -35,16 +35,20 @@ module TwoFactorAuthenticationConcern
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:email].present?
user = self.resource = find_user_from_params
prompt_for_two_factor(user) if user&.external_or_valid_password?(user_params[:password])
elsif session[:attempt_user_id]
user = self.resource = User.find_by(id: session[:attempt_user_id])
return if user.nil?
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user.webauthn_enabled? && user_params.key?(:credential) && session[:attempt_user_id]
authenticate_with_two_factor_via_webauthn(user)
elsif user_params.key?(:otp_attempt) && session[:attempt_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_two_factor(user)
if session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user.webauthn_enabled? && user_params.key?(:credential)
authenticate_with_two_factor_via_webauthn(user)
elsif user_params.key?(:otp_attempt)
authenticate_with_two_factor_via_otp(user)
end
end
end
@ -53,7 +57,6 @@ module TwoFactorAuthenticationConcern
if valid_webauthn_credential?(user, webauthn_credential)
clear_attempt_from_session
remember_me(user)
sign_in(user)
render json: { redirect_path: root_path }, status: :ok
else
@ -64,7 +67,6 @@ module TwoFactorAuthenticationConcern
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
clear_attempt_from_session
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_otp_token')

5
app/controllers/custom_css_controller.rb

@ -3,11 +3,16 @@
class CustomCssController < ApplicationController
skip_before_action :store_current_location
skip_before_action :require_functional!
skip_before_action :update_user_sign_in
skip_before_action :set_session_activity
skip_around_action :set_locale
before_action :set_cache_headers
def show
expires_in 3.minutes, public: true
request.session_options[:skip] = true
render plain: Setting.custom_css || '', content_type: 'text/css'
end
end

2
app/controllers/follower_accounts_controller.rb

@ -85,7 +85,7 @@ class FollowerAccountsController < ApplicationController
if page_requested? || !@account.user_hides_network?
# Return all fields
else
%i(id type totalItems)
%i(id type total_items)
end
end
end

2
app/controllers/following_accounts_controller.rb

@ -85,7 +85,7 @@ class FollowingAccountsController < ApplicationController
if page_requested? || !@account.user_hides_network?
# Return all fields
else
%i(id type totalItems)
%i(id type total_items)
end
end
end

2
app/controllers/media_proxy_controller.rb

@ -45,7 +45,7 @@ class MediaProxyController < ApplicationController
end
def lock_options
{ redis: Redis.current, key: "media_download:#{params[:id]}" }
{ redis: Redis.current, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds }
end
def reject_media?

2
app/controllers/settings/deletes_controller.rb

@ -42,7 +42,7 @@ class Settings::DeletesController < Settings::BaseController
end
def destroy_account!
current_account.suspend!(origin: :local)
current_account.suspend!(origin: :local, block_email: false)
AccountDeletionWorker.perform_async(current_user.account_id)
sign_out
end

3
app/controllers/well_known/webfinger_controller.rb

@ -4,7 +4,6 @@ module WellKnown
class WebfingerController < ActionController::Base
include RoutingHelper
before_action { response.headers['Vary'] = 'Accept' }
before_action :set_account
before_action :check_account_suspension
@ -39,10 +38,12 @@ module WellKnown
end
def bad_request
expires_in(3.minutes, public: true)
head 400
end
def not_found
expires_in(3.minutes, public: true)
head 404
end

6
app/helpers/accounts_helper.rb

@ -80,17 +80,17 @@ module AccountsHelper
def account_description(account)
prepend_str = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
number_to_human(account.statuses_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
].join(' '),
[
number_to_human(account.following_count, strip_insignificant_zeros: true),
number_to_human(account.following_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
[
number_to_human(account.followers_count, strip_insignificant_zeros: true),
number_to_human(account.followers_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' '),
].join(', ')

11
app/helpers/application_helper.rb

@ -14,6 +14,17 @@ module ApplicationHelper
ku
).freeze
def friendly_number_to_human(number, **options)
# By default, the number of precision digits used by number_to_human
# is looked up from the locales definition, and rails-i18n comes with
# values that don't seem to make much sense for many languages, so
# override these values with a default of 3 digits of precision.
options[:precision] = 3
options[:strip_insignificant_zeros] = true
number_to_human(number, **options)
end
def active_nav_class(*paths)
paths.any? { |path| current_page?(path) } ? 'active' : ''
end

55
app/helpers/context_helper.rb

@ -0,0 +1,55 @@
# frozen_string_literal: true
module ContextHelper
NAMED_CONTEXT_MAP = {
activitystreams: 'https://www.w3.org/ns/activitystreams',
security: 'https://w3id.org/security/v1',
}.freeze
CONTEXT_EXTENSION_MAP = {
manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' },
sensitive: { 'sensitive' => 'as:sensitive' },
hashtag: { 'Hashtag' => 'as:Hashtag' },
moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } },
also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } },
emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' },
featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, 'featuredTags' => { '@id' => 'toot:featuredTags', '@type' => '@id' } },
property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' },
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
}.freeze
def full_context
serialized_context(NAMED_CONTEXT_MAP, CONTEXT_EXTENSION_MAP)
end
def serialized_context(named_contexts_map, context_extensions_map)
context_array = []
named_contexts = named_contexts_map.keys
context_extensions = context_extensions_map.keys
named_contexts.each do |key|
context_array << NAMED_CONTEXT_MAP[key]
end
extensions = context_extensions.each_with_object({}) do |key, h|
h.merge!(CONTEXT_EXTENSION_MAP[key])
end
context_array << extensions unless extensions.empty?
if context_array.size == 1
context_array.first
else
context_array
end
end
end

80
app/helpers/jsonld_helper.rb

@ -1,6 +1,8 @@
# frozen_string_literal: true
module JsonLdHelper
include ContextHelper
def equals_or_includes?(haystack, needle)
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
end
@ -63,6 +65,84 @@ module JsonLdHelper
graph.dump(:normalize)
end
def compact(json)
compacted = JSON::LD::API.compact(json.without('signature'), full_context, documentLoader: method(:load_jsonld_context))
compacted['signature'] = json['signature']
compacted
end
# Patches a JSON-LD document to avoid compatibility issues on redistribution
#
# Since compacting a JSON-LD document against Mastodon's built-in vocabulary
# means other extension namespaces will be expanded, malformed JSON-LD
# attributes lost, and some values “unexpectedly” compacted this method
# patches the following likely sources of incompatibility:
# - 'https://www.w3.org/ns/activitystreams#Public' being compacted to
# 'as:Public' (for instance, pre-3.4.0 Mastodon does not understand
# 'as:Public')
# - single-item arrays being compacted to the item itself (`[foo]` being
# compacted to `foo`)
#
# It is not always possible for `patch_for_forwarding!` to produce a document
# deemed safe for forwarding. Use `safe_for_forwarding?` to check the status
# of the output document.
#
# @param original [Hash] The original JSON-LD document used as reference
# @param compacted [Hash] The compacted JSON-LD document to be patched
# @return [void]
def patch_for_forwarding!(original, compacted)
original.without('@context', 'signature').each do |key, value|
next if value.nil? || !compacted.key?(key)
compacted_value = compacted[key]
if value.is_a?(Hash) && compacted_value.is_a?(Hash)
patch_for_forwarding!(value, compacted_value)
elsif value.is_a?(Array)
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
return if value.size != compacted_value.size
compacted[key] = value.zip(compacted_value).map do |v, vc|
if v.is_a?(Hash) && vc.is_a?(Hash)
patch_for_forwarding!(v, vc)
vc
elsif v == 'https://www.w3.org/ns/activitystreams#Public' && vc == 'as:Public'
v
else
vc
end
end
elsif value == 'https://www.w3.org/ns/activitystreams#Public' && compacted_value == 'as:Public'
compacted[key] = value
end
end
end
# Tests whether a JSON-LD compaction is deemed safe for redistribution,
# that is, if it doesn't change its meaning to consumers that do not actually
# handle JSON-LD, but rely on values being serialized in a certain way.
#
# See `patch_for_forwarding!` for details.
#
# @param original [Hash] The original JSON-LD document used as reference
# @param compacted [Hash] The compacted JSON-LD document to be patched
# @return [Boolean] Whether the patched document is deemed safe
def safe_for_forwarding?(original, compacted)
original.without('@context', 'signature').all? do |key, value|
compacted_value = compacted[key]
return false unless value.class == compacted_value.class
if value.is_a?(Hash)
safe_for_forwarding?(value, compacted_value)
elsif value.is_a?(Array)
value.zip(compacted_value).all? do |v, vc|
v.is_a?(Hash) ? (vc.is_a?(Hash) && safe_for_forwarding?(v, vc)) : v == vc
end
else
value == compacted_value
end
end
end
def fetch_resource(uri, id, on_behalf_of = nil)
unless id
json = fetch_resource_without_id_validation(uri, on_behalf_of)

1
app/helpers/settings_helper.rb

@ -18,6 +18,7 @@ module SettingsHelper
en: 'English',
eo: 'Esperanto',
'es-AR': 'Español (Argentina)',
'es-MX': 'Español (México)',
es: 'Español',
et: 'Eesti',
eu: 'Euskara',

21
app/javascript/mastodon/actions/picture_in_picture.js

@ -22,13 +22,20 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
* @param {MediaProps} props
* @return {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
return (dispatch, getState) => {
// Do not open a player for a toot that does not exist
if (getState().hasIn(['statuses', statusId])) {
dispatch({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
}
};
};
/*
* @return {object}

2
app/javascript/mastodon/components/intersection_observer_article.js

@ -93,7 +93,7 @@ export default class IntersectionObserverArticle extends React.Component {
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
// See: https://github.com/tootsuite/mastodon/issues/2900
// See: https://github.com/mastodon/mastodon/issues/2900
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}

40
app/javascript/mastodon/components/modal_root.js

@ -1,10 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import 'wicg-inert';
import { createBrowserHistory } from 'history';
import { multiply } from 'color-blend';
export default class ModalRoot extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
children: PropTypes.node,
onClose: PropTypes.func.isRequired,
@ -48,6 +53,7 @@ export default class ModalRoot extends React.PureComponent {
componentDidMount () {
window.addEventListener('keyup', this.handleKeyUp, false);
window.addEventListener('keydown', this.handleKeyDown, false);
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
}
componentWillReceiveProps (nextProps) {
@ -69,6 +75,14 @@ export default class ModalRoot extends React.PureComponent {
this.activeElement.focus({ preventScroll: true });
this.activeElement = null;
}).catch(console.error);
this._handleModalClose();
}
if (this.props.children && !prevProps.children) {
this._handleModalOpen();
}
if (this.props.children) {
this._ensureHistoryBuffer();
}
}
@ -77,6 +91,32 @@ export default class ModalRoot extends React.PureComponent {
window.removeEventListener('keydown', this.handleKeyDown);
}
_handleModalOpen () {
this._modalHistoryKey = Date.now();
this.unlistenHistory = this.history.listen((_, action) => {
if (action === 'POP') {
this.props.onClose();
}
});
}
_handleModalClose () {
if (this.unlistenHistory) {
this.unlistenHistory();
}
const { state } = this.history.location;
if (state && state.mastodonModalKey === this._modalHistoryKey) {
this.history.goBack();
}
}
_ensureHistoryBuffer () {
const { pathname, state } = this.history.location;
if (!state || state.mastodonModalKey !== this._modalHistoryKey) {
this.history.push(pathname, { ...state, mastodonModalKey: this._modalHistoryKey });
}
}
getSiblings = () => {
return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
}

7
app/javascript/mastodon/components/scrollable_list.js

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll-4';
import ScrollContainer from 'mastodon/containers/scroll_container';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';
@ -34,7 +34,6 @@ class ScrollableList extends PureComponent {
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
showLoading: PropTypes.bool,
hasMore: PropTypes.bool,
@ -290,7 +289,7 @@ class ScrollableList extends PureComponent {
}
render () {
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state;
const childrenCount = React.Children.count(children);
@ -356,7 +355,7 @@ class ScrollableList extends PureComponent {
if (trackScroll) {
return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
<ScrollContainer scrollKey={scrollKey}>
{scrollableArea}
</ScrollContainer>
);

4
app/javascript/mastodon/components/status.js

@ -309,8 +309,8 @@ class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span>
</div>
</HotKeys>
);

5
app/javascript/mastodon/components/status_list.js

@ -18,7 +18,6 @@ export default class StatusList extends ImmutablePureComponent {
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
isPartial: PropTypes.bool,
hasMore: PropTypes.bool,
@ -77,7 +76,7 @@ export default class StatusList extends ImmutablePureComponent {
}
render () {
const { statusIds, featuredStatusIds, shouldUpdateScroll, onLoadMore, timelineId, ...other } = this.props;
const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other } = this.props;
const { isLoading, isPartial } = other;
if (isPartial) {
@ -120,7 +119,7 @@ export default class StatusList extends ImmutablePureComponent {
}
return (
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} shouldUpdateScroll={shouldUpdateScroll} ref={this.setRef}>
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
{scrollableContent}
</ScrollableList>
);

6
app/javascript/mastodon/containers/mastodon.js

@ -10,8 +10,6 @@ import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import { previewState as previewMediaState } from 'mastodon/features/ui/components/media_modal';
import { previewState as previewVideoState } from 'mastodon/features/ui/components/video_modal';
import initialState from '../initial_state';
import ErrorBoundary from '../components/error_boundary';
@ -41,8 +39,8 @@ export default class Mastodon extends React.PureComponent {
}
}
shouldUpdateScroll (_, { location }) {
return location.state !== previewMediaState && location.state !== previewVideoState;
shouldUpdateScroll (prevRouterProps, { location }) {
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
}
render () {

18
app/javascript/mastodon/containers/scroll_container.js

@ -0,0 +1,18 @@
import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4';
// ScrollContainer is used to automatically scroll to the top when pushing a
// new history state and remembering the scroll position when going back.
// There are a few things we need to do differently, though.
const defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
// If the change is caused by opening a modal, do not scroll to top
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
};
export default
class ScrollContainer extends OriginalScrollContainer {
static defaultProps = {
shouldUpdateScroll: defaultShouldUpdateScroll,
};
}

7
app/javascript/mastodon/features/account_gallery/index.js

@ -11,7 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from 'mastodon/selectors';
import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container';
import { ScrollContainer } from 'react-router-scroll-4';
import ScrollContainer from 'mastodon/containers/scroll_container';
import LoadMore from 'mastodon/components/load_more';
import MissingIndicator from 'mastodon/components/missing_indicator';
import { openModal } from 'mastodon/actions/modal';
@ -29,7 +29,6 @@ const mapStateToProps = (state, props) => ({
class LoadMoreMedia extends ImmutablePureComponent {
static propTypes = {
shouldUpdateScroll: PropTypes.func,
maxId: PropTypes.string,
onLoadMore: PropTypes.func.isRequired,
};
@ -127,7 +126,7 @@ class AccountGallery extends ImmutablePureComponent {
}
render () {
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
const { width } = this.state;
if (!isAccount) {
@ -164,7 +163,7 @@ class AccountGallery extends ImmutablePureComponent {
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}>
<ScrollContainer scrollKey='account_gallery'>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} />

4
app/javascript/mastodon/features/account_timeline/index.js

@ -50,7 +50,6 @@ class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
@ -115,7 +114,7 @@ class AccountTimeline extends ImmutablePureComponent {
}
render () {
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
const { statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
@ -162,7 +161,6 @@ class AccountTimeline extends ImmutablePureComponent {
isLoading={isLoading}
hasMore={hasMore}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='account'

4
app/javascript/mastodon/features/blocks/index.js

@ -29,7 +29,6 @@ class Blocks extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
@ -46,7 +45,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, accountIds, shouldUpdateScroll, hasMore, multiColumn, isLoading } = this.props;
const { intl, accountIds, hasMore, multiColumn, isLoading } = this.props;
if (!accountIds) {
return (
@ -66,7 +65,6 @@ class Blocks extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

4
app/javascript/mastodon/features/bookmarked_statuses/index.js

@ -27,7 +27,6 @@ class Bookmarks extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
@ -68,7 +67,7 @@ class Bookmarks extends ImmutablePureComponent {
}, 300, { leading: true })
render () {
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />;
@ -93,7 +92,6 @@ class Bookmarks extends ImmutablePureComponent {
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>

4
app/javascript/mastodon/features/community_timeline/index.js

@ -41,7 +41,6 @@ class CommunityTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
@ -103,7 +102,7 @@ class CommunityTimeline extends React.PureComponent {
}
render () {
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
const pinned = !!columnId;
return (
@ -127,7 +126,6 @@ class CommunityTimeline extends React.PureComponent {
timelineId={`community${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

25
app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
import Overlay from 'react-overlays/lib/Overlay';
import classNames from 'classnames';
@ -12,7 +12,6 @@ import { assetHost } from 'mastodon/utils/config';
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
@ -28,9 +27,26 @@ const messages = defineMessages({
let EmojiPicker, Emoji; // load asynchronously
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
const notFoundFn = () => (
<div className='emoji-mart-no-results'>
<Emoji
emoji='sleuth_or_spy'
set='twitter'
size={32}
sheetSize={32}
backgroundImageFn={backgroundImageFn}
/>
<div className='emoji-mart-no-results-label'>
<FormattedMessage id='emoji_button.not_found' defaultMessage='No matching emojis found' />
</div>
</div>
);
class ModifierPickerMenu extends React.PureComponent {
static propTypes = {
@ -182,7 +198,6 @@ class EmojiPickerMenu extends React.PureComponent {
return {
search: intl.formatMessage(messages.emoji_search),
notfound: intl.formatMessage(messages.emoji_not_found),
categories: {
search: intl.formatMessage(messages.search_results),
recent: intl.formatMessage(messages.recent),
@ -263,7 +278,9 @@ class EmojiPickerMenu extends React.PureComponent {
recent={frequentlyUsedEmojis}
skin={skinTone}
showPreview={false}
showSkinTones={false}
backgroundImageFn={backgroundImageFn}
notFound={notFoundFn}
autoFocus
emojiTooltip
/>

1
app/javascript/mastodon/features/compose/containers/navigation_container.js

@ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
}));
},

1
app/javascript/mastodon/features/compose/index.js

@ -93,6 +93,7 @@ class Compose extends React.PureComponent {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
}));

1
app/javascript/mastodon/features/direct_timeline/components/conversations_list.js

@ -14,7 +14,6 @@ export default class ConversationsList extends ImmutablePureComponent {
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
onLoadMore: PropTypes.func,
shouldUpdateScroll: PropTypes.func,
};
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id)

4
app/javascript/mastodon/features/direct_timeline/index.js

@ -19,7 +19,6 @@ class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
@ -71,7 +70,7 @@ class DirectTimeline extends React.PureComponent {
}
render () {
const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props;
const { intl, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return (
@ -93,7 +92,6 @@ class DirectTimeline extends React.PureComponent {
timelineId='direct'
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
shouldUpdateScroll={shouldUpdateScroll}
/>
</Column>
);

7
app/javascript/mastodon/features/directory/index.js

@ -12,7 +12,7 @@ import AccountCard from './components/account_card';
import RadioButton from 'mastodon/components/radio_button';
import classNames from 'classnames';
import LoadMore from 'mastodon/components/load_more';
import { ScrollContainer } from 'react-router-scroll-4';
import ScrollContainer from 'mastodon/containers/scroll_container';
const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
@ -40,7 +40,6 @@ class Directory extends React.PureComponent {
isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
@ -125,7 +124,7 @@ class Directory extends React.PureComponent {
}
render () {
const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props;
const { isLoading, accountIds, intl, columnId, multiColumn, domain } = this.props;
const { order, local } = this.getParams(this.props, this.state);
const pinned = !!columnId;
@ -163,7 +162,7 @@ class Directory extends React.PureComponent {
multiColumn={multiColumn}
/>
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea}
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
</Column>
);
}

4
app/javascript/mastodon/features/domain_blocks/index.js

@ -29,7 +29,6 @@ class Blocks extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
domains: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired,
@ -45,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, domains, shouldUpdateScroll, hasMore, multiColumn } = this.props;
const { intl, domains, hasMore, multiColumn } = this.props;
if (!domains) {
return (
@ -64,7 +63,6 @@ class Blocks extends ImmutablePureComponent {
scrollKey='domain_blocks'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

33
app/javascript/mastodon/features/emoji/emoji_compressed.js

@ -7,29 +7,38 @@
const { unicodeToFilename } = require('./unicode_to_filename');
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
const emojiMap = require('./emoji_map.json');
const emojiMap = require('./emoji_map.json');
const { emojiIndex } = require('emoji-mart');
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
let data = require('emoji-mart/data/all.json');
if(data.compressed) {
data = emojiMartUncompress(data);
}
const emojiMartData = data;
const excluded = ['®', '©', '™'];
const skins = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const shortcodeMap = {};
const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = [];
Object.keys(emojiIndex.emojis).forEach(key => {
shortcodeMap[emojiIndex.emojis[key].native] = emojiIndex.emojis[key].id;
let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this
if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
emoji = emoji['1'];
}
shortcodeMap[emoji.native] = emoji.id;
});
const stripModifiers = unicode => {
skins.forEach(tone => {
skinTones.forEach(tone => {
unicode = unicode.replace(tone, '');
});
@ -64,13 +73,22 @@ Object.keys(emojiMap).forEach(key => {
if (!Array.isArray(shortCodesToEmojiData[shortcode])) {
shortCodesToEmojiData[shortcode] = [[]];
}
shortCodesToEmojiData[shortcode][0].push(filenameData);
}
});
Object.keys(emojiIndex.emojis).forEach(key => {
const { native } = emojiIndex.emojis[key];
let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this
if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
emoji = emoji['1'];
}
const { native } = emoji;
let { short_names, search, unified } = emojiMartData.emojis[key];
if (short_names[0] !== key) {
throw new Error('The compresser expects the first short_code to be the ' +
'key. It may need to be rewritten if the emoji change such that this ' +
@ -80,11 +98,16 @@ Object.keys(emojiIndex.emojis).forEach(key => {
short_names = short_names.slice(1); // first short name can be inferred from the key
const searchData = [native, short_names, search];
if (unicodeToUnifiedName(native) !== unified) {
// unified name can't be derived from unicodeToUnifiedName
searchData.push(unified);
}
if (!Array.isArray(shortCodesToEmojiData[key])) {
shortCodesToEmojiData[key] = [[]];
}
shortCodesToEmojiData[key].push(searchData);
});

4
app/javascript/mastodon/features/emoji/unicode_to_unified_name.js

@ -2,16 +2,20 @@ function padLeft(str, num) {
while (str.length < num) {
str = '0' + str;
}
return str;
}
exports.unicodeToUnifiedName = (str) => {
let output = '';
for (let i = 0; i < str.length; i += 2) {
if (i > 0) {
output += '-';
}
output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4);
}
return output;
};

4
app/javascript/mastodon/features/favourited_statuses/index.js

@ -27,7 +27,6 @@ class Favourites extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
@ -68,7 +67,7 @@ class Favourites extends ImmutablePureComponent {
}, 300, { leading: true })
render () {
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite posts yet. When you favourite one, it will show up here." />;
@ -93,7 +92,6 @@ class Favourites extends ImmutablePureComponent {
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>

4
app/javascript/mastodon/features/favourites/index.js

@ -27,7 +27,6 @@ class Favourites extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
@ -50,7 +49,7 @@ class Favourites extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
const { intl, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
@ -74,7 +73,6 @@ class Favourites extends ImmutablePureComponent {
<ScrollableList
scrollKey='favourites'
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

4
app/javascript/mastodon/features/follow_requests/index.js

@ -32,7 +32,6 @@ class FollowRequests extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
@ -51,7 +50,7 @@ class FollowRequests extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn, locked, domain, isLoading } = this.props;
const { intl, accountIds, hasMore, multiColumn, locked, domain, isLoading } = this.props;
if (!accountIds) {
return (
@ -80,7 +79,6 @@ class FollowRequests extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
prepend={unlockedPrependMessage}

4
app/javascript/mastodon/features/followers/index.js

@ -43,7 +43,6 @@ class Followers extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
@ -73,7 +72,7 @@ class Followers extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
@ -112,7 +111,6 @@ class Followers extends ImmutablePureComponent {
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
alwaysPrepend
append={remoteMessage}

4
app/javascript/mastodon/features/following/index.js

@ -43,7 +43,6 @@ class Following extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
@ -73,7 +72,7 @@ class Following extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
@ -112,7 +111,6 @@ class Following extends ImmutablePureComponent {
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
alwaysPrepend
append={remoteMessage}

4
app/javascript/mastodon/features/hashtag_timeline/index.js

@ -24,7 +24,6 @@ class HashtagTimeline extends React.PureComponent {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
@ -130,7 +129,7 @@ class HashtagTimeline extends React.PureComponent {
}
render () {
const { shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props;
const { hasUnread, columnId, multiColumn } = this.props;
const { id, local } = this.props.params;
const pinned = !!columnId;
@ -156,7 +155,6 @@ class HashtagTimeline extends React.PureComponent {
timelineId={`hashtag:${id}${local ? ':local' : ''}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

4
app/javascript/mastodon/features/home_timeline/index.js

@ -34,7 +34,6 @@ class HomeTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
isPartial: PropTypes.bool,
@ -112,7 +111,7 @@ class HomeTimeline extends React.PureComponent {
}
render () {
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const pinned = !!columnId;
let announcementsButton = null;
@ -154,7 +153,6 @@ class HomeTimeline extends React.PureComponent {
onLoadMore={this.handleLoadMore}
timelineId='home'
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

4
app/javascript/mastodon/features/list_timeline/index.js

@ -41,7 +41,6 @@ class ListTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
@ -142,7 +141,7 @@ class ListTimeline extends React.PureComponent {
}
render () {
const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list, intl } = this.props;
const { hasUnread, columnId, multiColumn, list, intl } = this.props;
const { id } = this.props.params;
const pinned = !!columnId;
const title = list ? list.get('title') : id;
@ -207,7 +206,6 @@ class ListTimeline extends React.PureComponent {
timelineId={`list:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

3
app/javascript/mastodon/features/lists/index.js

@ -48,7 +48,7 @@ class Lists extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, lists, multiColumn } = this.props;
const { intl, lists, multiColumn } = this.props;
if (!lists) {
return (
@ -68,7 +68,6 @@ class Lists extends ImmutablePureComponent {
<ScrollableList
scrollKey='lists'
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
bindToDocument={!multiColumn}

4
app/javascript/mastodon/features/mutes/index.js

@ -29,7 +29,6 @@ class Mutes extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
@ -46,7 +45,7 @@ class Mutes extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, shouldUpdateScroll, hasMore, accountIds, multiColumn, isLoading } = this.props;
const { intl, hasMore, accountIds, multiColumn, isLoading } = this.props;
if (!accountIds) {
return (
@ -66,7 +65,6 @@ class Mutes extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

4
app/javascript/mastodon/features/notifications/index.js

@ -74,7 +74,6 @@ class Notifications extends React.PureComponent {
notifications: ImmutablePropTypes.list.isRequired,
showFilterBar: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired,
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
@ -176,7 +175,7 @@ class Notifications extends React.PureComponent {
};
render () {
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
@ -227,7 +226,6 @@ class Notifications extends React.PureComponent {
onLoadPending={this.handleLoadPending}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
>
{scrollableContent}

6
app/javascript/mastodon/features/picture_in_picture/components/footer.js

@ -114,7 +114,11 @@ class Footer extends ImmutablePureComponent {
return;
}
const { status } = this.props;
const { status, onClose } = this.props;
if (onClose) {
onClose();
}
router.history.push(`/statuses/${status.get('id')}`);
}

4
app/javascript/mastodon/features/pinned_statuses/index.js

@ -24,7 +24,6 @@ class PinnedStatuses extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool.isRequired,
@ -44,7 +43,7 @@ class PinnedStatuses extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, statusIds, hasMore, multiColumn } = this.props;
const { intl, statusIds, hasMore, multiColumn } = this.props;
return (
<Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
@ -53,7 +52,6 @@ class PinnedStatuses extends ImmutablePureComponent {
statusIds={statusIds}
scrollKey='pinned_statuses'
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

4
app/javascript/mastodon/features/public_timeline/index.js

@ -43,7 +43,6 @@ class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
@ -106,7 +105,7 @@ class PublicTimeline extends React.PureComponent {
}
render () {
const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const pinned = !!columnId;
return (
@ -130,7 +129,6 @@ class PublicTimeline extends React.PureComponent {
trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

4
app/javascript/mastodon/features/reblogs/index.js

@ -27,7 +27,6 @@ class Reblogs extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
@ -50,7 +49,7 @@ class Reblogs extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
const { intl, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
@ -74,7 +73,6 @@ class Reblogs extends ImmutablePureComponent {
<ScrollableList
scrollKey='reblogs'
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

12
app/javascript/mastodon/features/status/index.js

@ -45,7 +45,7 @@ import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts';
import { initReport } from '../../actions/reports';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { ScrollContainer } from 'react-router-scroll-4';
import ScrollContainer from 'mastodon/containers/scroll_container';
import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container';
@ -83,7 +83,7 @@ const makeMapStateToProps = () => {
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id) {
while (id && !mutable.includes(id)) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
@ -101,7 +101,7 @@ const makeMapStateToProps = () => {
const ids = [statusId];
while (ids.length > 0) {
let id = ids.shift();
let id = ids.pop();
const replies = contextReplies.get(id);
if (statusId !== id) {
@ -110,7 +110,7 @@ const makeMapStateToProps = () => {
if (replies) {
replies.reverse().forEach(reply => {
ids.unshift(reply);
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
});
}
}
@ -498,7 +498,7 @@ class Status extends ImmutablePureComponent {
render () {
let ancestors, descendants;
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
if (status === null) {
@ -541,7 +541,7 @@ class Status extends ImmutablePureComponent {
)}
/>
<ScrollContainer scrollKey='thread' shouldUpdateScroll={shouldUpdateScroll}>
<ScrollContainer scrollKey='thread'>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
{ancestors}

27
app/javascript/mastodon/features/ui/components/audio_modal.js

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { previewState } from './video_modal';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { statusId }) => ({
@ -25,32 +24,6 @@ class AudioModal extends ImmutablePureComponent {
onChangeBackgroundColor: PropTypes.func.isRequired,
};
static contextTypes = {
router: PropTypes.object,
};
componentDidMount () {
if (this.context.router) {
const history = this.context.router.history;
history.push(history.location.pathname, previewState);
this.unlistenHistory = history.listen(() => {
this.props.onClose();
});
}
}
componentWillUnmount () {
if (this.context.router) {
this.unlistenHistory();
if (this.context.router.history.location.state === previewState) {
this.context.router.history.goBack();
}
}
}
render () {
const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {};

9
app/javascript/mastodon/features/ui/components/confirmation_modal.js

@ -13,15 +13,22 @@ class ConfirmationModal extends React.PureComponent {
onConfirm: PropTypes.func.isRequired,
secondary: PropTypes.string,
onSecondary: PropTypes.func,
closeWhenConfirm: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
static defaultProps = {
closeWhenConfirm: true,
};
componentDidMount() {
this.button.focus();
}
handleClick = () => {
this.props.onClose();
if (this.props.closeWhenConfirm) {
this.props.onClose();
}
this.props.onConfirm();
}

1
app/javascript/mastodon/features/ui/components/link_footer.js

@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
}));
},

24
app/javascript/mastodon/features/ui/components/media_modal.js

@ -20,8 +20,6 @@ const messages = defineMessages({
next: { id: 'lightbox.next', defaultMessage: 'Next' },
});
export const previewState = 'previewMediaModal';
export default @injectIntl
class MediaModal extends ImmutablePureComponent {
@ -37,10 +35,6 @@ class MediaModal extends ImmutablePureComponent {
volume: PropTypes.number,
};
static contextTypes = {
router: PropTypes.object,
};
state = {
index: null,
navigationHidden: false,
@ -98,16 +92,6 @@ class MediaModal extends ImmutablePureComponent {
componentDidMount () {
window.addEventListener('keydown', this.handleKeyDown, false);
if (this.context.router) {
const history = this.context.router.history;
history.push(history.location.pathname, previewState);
this.unlistenHistory = history.listen(() => {
this.props.onClose();
});
}
this._sendBackgroundColor();
}
@ -131,14 +115,6 @@ class MediaModal extends ImmutablePureComponent {
componentWillUnmount () {
window.removeEventListener('keydown', this.handleKeyDown);
if (this.context.router) {
this.unlistenHistory();
if (this.context.router.history.location.state === previewState) {
this.context.router.history.goBack();
}
}
this.props.onChangeBackgroundColor(null);
}

24
app/javascript/mastodon/features/ui/components/video_modal.js

@ -6,8 +6,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
export const previewState = 'previewVideoModal';
export default class VideoModal extends ImmutablePureComponent {
static propTypes = {
@ -22,19 +20,9 @@ export default class VideoModal extends ImmutablePureComponent {
onChangeBackgroundColor: PropTypes.func.isRequired,
};
static contextTypes = {
router: PropTypes.object,
};
componentDidMount () {
const { router } = this.context;
const { media, onChangeBackgroundColor, onClose } = this.props;
if (router) {
router.history.push(router.history.location.pathname, previewState);
this.unlistenHistory = router.history.listen(() => onClose());
}
const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
if (backgroundColor) {
@ -42,18 +30,6 @@ export default class VideoModal extends ImmutablePureComponent {
}
}
componentWillUnmount () {
const { router } = this.context;
if (router) {
this.unlistenHistory();
if (router.history.location.state === previewState) {
router.history.goBack();
}
}
}
render () {
const { media, statusId, onClose } = this.props;
const options = this.props.options || {};

4
app/javascript/mastodon/features/ui/containers/modal_container.js

@ -3,8 +3,8 @@ import { closeModal } from '../../../actions/modal';
import ModalRoot from '../components/modal_root';
const mapStateToProps = state => ({
type: state.get('modal').modalType,
props: state.get('modal').modalProps,
type: state.getIn(['modal', 0, 'modalType'], null),
props: state.getIn(['modal', 0, 'modalProps'], {}),
});
const mapDispatchToProps = dispatch => ({

60
app/javascript/mastodon/features/ui/index.js

@ -54,8 +54,6 @@ import {
FollowRecommendations,
} from './util/async-components';
import { me } from '../../initial_state';
import { previewState as previewMediaState } from './components/media_modal';
import { previewState as previewVideoState } from './components/video_modal';
import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
@ -138,10 +136,6 @@ class SwitchingColumnsArea extends React.PureComponent {
}
}
shouldUpdateScroll (_, { location }) {
return location.state !== previewMediaState && location.state !== previewVideoState;
}
setRef = c => {
if (c) {
this.node = c.getWrappedInstance();
@ -158,38 +152,38 @@ class SwitchingColumnsArea extends React.PureComponent {
{redirect}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/notifications' component={Notifications} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
<WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/directory' component={Directory} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/blocks' component={Blocks} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/mutes' component={Mutes} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/lists' component={Lists} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
<WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} />
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} />
<WrappedRoute component={GenericNotFound} content={children} />
</WrappedSwitch>
@ -361,9 +355,9 @@ class UI extends React.PureComponent {
this.props.dispatch(closeOnboarding());
}
this.props.dispatch(fetchMarkers());
this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications());
setTimeout(() => this.props.dispatch(fetchMarkers()), 500);
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {

2
app/javascript/mastodon/locales/af.json

@ -142,7 +142,7 @@
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",

2
app/javascript/mastodon/locales/cs.json

@ -31,7 +31,7 @@
"account.moved_to": "Uživatel {name} se přesunul na:",
"account.mute": "Skrýt @{name}",
"account.mute_notifications": "Skrýt oznámení od @{name}",
"account.muted": "Účet skryt",
"account.muted": "Skryt",
"account.never_active": "Nikdy",
"account.posts": "Příspěvky",
"account.posts_with_replies": "Příspěvky a odpovědi",

8
app/javascript/mastodon/locales/defaultMessages.json

@ -1078,10 +1078,6 @@
"defaultMessage": "Search...",
"id": "emoji_button.search"
},
{
"defaultMessage": "No emojos!! (╯°□°)╯︵ ┻━┻",
"id": "emoji_button.not_found"
},
{
"defaultMessage": "Custom",
"id": "emoji_button.custom"
@ -1125,6 +1121,10 @@
{
"defaultMessage": "Flags",
"id": "emoji_button.flags"
},
{
"defaultMessage": "No matching emojis found",
"id": "emoji_button.not_found"
}
],
"path": "app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.json"

2
app/javascript/mastodon/locales/en.json

@ -142,7 +142,7 @@
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",

2
app/javascript/mastodon/locales/es-AR.json

@ -142,7 +142,7 @@
"emoji_button.food": "Comida y bebida",
"emoji_button.label": "Insertar emoji",
"emoji_button.nature": "Naturaleza",
"emoji_button.not_found": "¡¡No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No se encontraron emojis coincidentes",
"emoji_button.objects": "Objetos",
"emoji_button.people": "Gente",
"emoji_button.recent": "Usados frecuentemente",

28
app/javascript/mastodon/locales/es-MX.json

@ -22,7 +22,7 @@
"account.follows.empty": "Este usuario todavía no sigue a nadie.",
"account.follows_you": "Te sigue",
"account.hide_reblogs": "Ocultar retoots de @{name}",
"account.joined": "Joined {date}",
"account.joined": "Se unió el {date}",
"account.last_status": "Última actividad",
"account.link_verified_on": "El proprietario de este link fue comprobado el {date}",
"account.locked_info": "El estado de privacidad de esta cuenta està configurado como bloqueado. El proprietario debe revisar manualmente quien puede seguirle.",
@ -98,7 +98,7 @@
"compose_form.poll.switch_to_multiple": "Modificar encuesta para permitir múltiples opciones",
"compose_form.poll.switch_to_single": "Modificar encuesta para permitir una única opción",
"compose_form.publish": "Tootear",
"compose_form.publish_loud": "{publish}!",
"compose_form.publish_loud": "¡{publish}!",
"compose_form.sensitive.hide": "Marcar multimedia como sensible",
"compose_form.sensitive.marked": "Material marcado como sensible",
"compose_form.sensitive.unmarked": "Material no marcado como sensible",
@ -160,11 +160,11 @@
"empty_column.domain_blocks": "Todavía no hay dominios ocultos.",
"empty_column.favourited_statuses": "Aún no tienes toots preferidos. Cuando marques uno como favorito, aparecerá aquí.",
"empty_column.favourites": "Nadie ha marcado este toot como preferido. Cuando alguien lo haga, aparecerá aquí.",
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"empty_column.follow_recommendations": "Parece que no se ha podido generar ninguna sugerencia para ti. Puedes probar a buscar a gente que quizá conozcas o explorar los hashtags que están en tendencia.",
"empty_column.follow_requests": "No tienes ninguna petición de seguidor. Cuando recibas una, se mostrará aquí.",
"empty_column.hashtag": "No hay nada en este hashtag aún.",
"empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
"empty_column.home.suggestions": "See some suggestions",
"empty_column.home.suggestions": "Ver algunas sugerencias",
"empty_column.list": "No hay nada en esta lista aún. Cuando miembros de esta lista publiquen nuevos estatus, estos aparecerán qui.",
"empty_column.lists": "No tienes ninguna lista. cuando crees una, se mostrará aquí.",
"empty_column.mutes": "Aún no has silenciado a ningún usuario.",
@ -177,8 +177,8 @@
"errors.unexpected_crash.copy_stacktrace": "Copiar el seguimiento de pila en el portapapeles",
"errors.unexpected_crash.report_issue": "Informar de un problema/error",
"follow_recommendations.done": "Hecho",
"follow_recommendations.heading": "¡Sigue a la gente cuyas publicaciones te gustaría ver! Aquí tienes algunas sugerencias.",
"follow_recommendations.lead": "Los mensajes de las personas que sigues aparecerán en orden cronológico en el Inicio. No tengas miedo de cometer errores, ¡puedes dejar de seguir a la gente fácilmente en cualquier momento!",
"follow_recommendations.heading": "¡Sigue a gente que publique cosas que te gusten! Aquí tienes algunas sugerencias.",
"follow_recommendations.lead": "Las publicaciones de la gente a la que sigas aparecerán ordenadas cronológicamente en Inicio. No tengas miedo de cometer errores, ¡puedes dejarles de seguir en cualquier momento con la misma facilidad!",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Rechazar",
"follow_requests.unlocked_explanation": "A pesar de que tu cuenta no es privada, el personal de {domain} ha pensado que quizás deberías revisar manualmente las solicitudes de seguimiento de estas cuentas.",
@ -335,7 +335,7 @@
"picture_in_picture.restore": "Restaurar",
"poll.closed": "Cerrada",
"poll.refresh": "Actualizar",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
"poll.total_people": "{count, plural, one {# persona} other {# personas}}",
"poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
"poll.vote": "Votar",
"poll.voted": "Has votado a favor de esta respuesta",
@ -353,11 +353,11 @@
"refresh": "Actualizar",
"regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.days": "{number} d",
"relative_time.hours": "{number} h",
"relative_time.just_now": "ahora",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.minutes": "{number} m",
"relative_time.seconds": "{number} s",
"relative_time.today": "hoy",
"reply_indicator.cancel": "Cancelar",
"report.forward": "Reenviar a {target}",
@ -439,9 +439,9 @@
"trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} personas}} hablando",
"trends.trending_now": "Tendencia ahora",
"ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"units.short.billion": "{count} MM",
"units.short.million": "{count} M",
"units.short.thousand": "{count} K",
"upload_area.title": "Arrastra y suelta para subir",
"upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Límite de subida de archivos excedido.",

18
app/javascript/mastodon/locales/es.json

@ -98,7 +98,7 @@
"compose_form.poll.switch_to_multiple": "Modificar encuesta para permitir múltiples opciones",
"compose_form.poll.switch_to_single": "Modificar encuesta para permitir una única opción",
"compose_form.publish": "Tootear",
"compose_form.publish_loud": "{publish}!",
"compose_form.publish_loud": "¡{publish}!",
"compose_form.sensitive.hide": "Marcar multimedia como sensible",
"compose_form.sensitive.marked": "Material marcado como sensible",
"compose_form.sensitive.unmarked": "Material no marcado como sensible",
@ -340,7 +340,7 @@
"picture_in_picture.restore": "Restaurar",
"poll.closed": "Cerrada",
"poll.refresh": "Actualizar",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
"poll.total_people": "{count, plural, one {# persona} other {# personas}}",
"poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
"poll.vote": "Votar",
"poll.voted": "Has votado a favor de esta respuesta",
@ -358,11 +358,11 @@
"refresh": "Actualizar",
"regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.days": "{number} d",
"relative_time.hours": "{number} h",
"relative_time.just_now": "ahora",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.minutes": "{number} m",
"relative_time.seconds": "{number} s",
"relative_time.today": "hoy",
"reply_indicator.cancel": "Cancelar",
"report.forward": "Reenviar a {target}",
@ -445,9 +445,9 @@
"trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} personas}} hablando",
"trends.trending_now": "Tendencia ahora",
"ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"units.short.billion": "{count} MM",
"units.short.million": "{count} M",
"units.short.thousand": "{count} K",
"upload_area.title": "Arrastra y suelta para subir",
"upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Límite de subida de archivos excedido.",

74
app/javascript/mastodon/locales/gd.json

@ -102,8 +102,8 @@
"compose_form.sensitive.hide": "{count, plural, one {Cuir comharra gu bheil am meadhan frionasach} two {Cuir comharra gu bheil na meadhanan frionasach} few {Cuir comharra gu bheil na meadhanan frionasach} other {Cuir comharra gu bheil na meadhanan frionasach}}",
"compose_form.sensitive.marked": "{count, plural, one {Tha comharra ris a’ mheadhan gu bheil e frionasach} two {Tha comharra ris na meadhanan gu bheil iad frionasach} few {Tha comharra ris na meadhanan gu bheil iad frionasach} other {Tha comharra ris na meadhanan gu bheil iad frionasach}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Chan eil comharra ris a’ mheadhan gun robh e frionasach} two {Chan eil comharra ris na meadhanan gun robh iad frionasach} few {Chan eil comharra ris na meadhanan gun robh iad frionasach} other {Chan eil comharra ris na meadhanan gun robh iad frionasach}}",
"compose_form.spoiler.marked": "Tha an teacsa falaichte air cùlaibh rabhaidh",
"compose_form.spoiler.unmarked": "Chan eil an teacsa flaichte",
"compose_form.spoiler.marked": "Thoir air falbh an rabhadh susbainte",
"compose_form.spoiler.unmarked": "Cuir rabhadh susbainte ris",
"compose_form.spoiler_placeholder": "Sgrìobh an rabhadh agad an-seo",
"confirmation_modal.cancel": "Sguir dheth",
"confirmations.block.block_and_report": "Bac ⁊ dèan gearan",
@ -142,7 +142,7 @@
"emoji_button.food": "Biadh ⁊ deoch",
"emoji_button.label": "Cuir a-steach Emoji",
"emoji_button.nature": "An nàdar",
"emoji_button.not_found": "Chan eil Emoji gnàthaichte ann!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "Cha deach Emoji iomchaidh a lorg",
"emoji_button.objects": "Nithean",
"emoji_button.people": "Daoine",
"emoji_button.recent": "Air a chleachdadh o chionn ghoirid",
@ -163,12 +163,12 @@
"empty_column.follow_recommendations": "Chan urrainn dhuinn dad a mholadh dhut. Cleachd gleus an luirg feuch an lorg thu daoine air a bheil thu eòlach no rùraich na tagaichean-hais a tha a’ treandadh.",
"empty_column.follow_requests": "Chan eil iarrtas air leantainn agad fhathast. Nuair gheibh thu fear, nochdaidh e an-seo.",
"empty_column.hashtag": "Chan eil dad san taga hais seo fhathast.",
"empty_column.home": "Tha an loidhne-ama dachaigh agad falamh! Tadhail air {public} no dèan lorg airson toiseach-tòiseachaidh a dhèanamh is tachairt ri càch.",
"empty_column.home": "Tha an loidhne-ama dachaigh agad falamh! Lean air barrachd dhaoine gus a lìonadh. {suggestions}",
"empty_column.home.suggestions": "Faic moladh no dhà",
"empty_column.list": "Chan eil dad air an liosta seo fhathast. Nuair a phostaicheas buill a tha air an liosta seo postaichean ùra, nochdaidh iad an-seo.",
"empty_column.lists": "Chan eil liosta agad fhathast. Nuair chruthaicheas tu tè, nochdaidh i an-seo.",
"empty_column.mutes": "Cha do mhùch thu cleachdaiche sam bith fhathast.",
"empty_column.notifications": "Cha d’ fhuair thu brath sam bith fhathast. Dèan eadar-ghnìomh le càch airson tòiseachadh air còmhradh.",
"empty_column.notifications": "Cha d’ fhuair thu brath sam bith fhathast. Nuair a ghabhas càch eadar-ghnìomh leat, chì thu an-seo e.",
"empty_column.public": "Chan eil dad an-seo! Sgrìobh rudeigin gu poblach no lean air càch o fhrithealaichean eile a làimh airson seo a lìonadh",
"error.unexpected_crash.explanation": "Air sàilleibh buga sa chòd againn no duilgheadas co-chòrdalachd leis a’ bhrabhsair, chan urrainn dhuinn an duilleag seo a shealltainn mar bu chòir.",
"error.unexpected_crash.explanation_addons": "Cha b’ urrainn dhuinn an duilleag seo a shealltainn mar bu chòir. Tha sinn an dùil gu do dh’adhbharaich tuilleadan a’ bhrabhsair no inneal eadar-theangachaidh fèin-obrachail a’ mhearachd.",
@ -208,40 +208,40 @@
"intervals.full.days": "{number, plural, one {# latha} two {# latha} few {# làithean} other {# latha}}",
"intervals.full.hours": "{number, plural, one {# uair a thìde} two {# uair a thìde} few {# uairean a thìde} other {# uair a thìde}}",
"intervals.full.minutes": "{number, plural, one {# mhionaid} two {# mhionaid} few {# mionaidean} other {# mionaid}}",
"keyboard_shortcuts.back": "a sheòladh air ais",
"keyboard_shortcuts.blocked": "a dh’fhosgladh liosta nan cleachdaichean bacte",
"keyboard_shortcuts.boost": "’ga bhrosnachadh",
"keyboard_shortcuts.column": "a chur am fòcas air post air fear dhe na colbhan",
"keyboard_shortcuts.compose": "a chur am fòcas air raon teacsa an sgrìobhaidh",
"keyboard_shortcuts.back": "Seòl air ais",
"keyboard_shortcuts.blocked": "Fosgail liosta nan cleachdaichean bacte",
"keyboard_shortcuts.boost": "Brosnaich post",
"keyboard_shortcuts.column": "Cuir am fòcas air colbh",
"keyboard_shortcuts.compose": "Cuir am fòcas air raon teacsa an sgrìobhaidh",
"keyboard_shortcuts.description": "Tuairisgeul",
"keyboard_shortcuts.direct": "a dh’fhosgladh colbh nan teachdaireachdan dìreach",
"keyboard_shortcuts.down": "a ghluasad sìos air an liosta",
"keyboard_shortcuts.enter": "a dh’fhosgladh a’ phuist",
"keyboard_shortcuts.favourite": "’ga chur ris na h-annsachdan",
"keyboard_shortcuts.favourites": "a dh’fhosgladh liosta nan annsachdan",
"keyboard_shortcuts.federated": "a dh’fhosgladh na loidhne-ama co-naisgte",
"keyboard_shortcuts.direct": "Fosgail colbh nan teachdaireachdan dìreach",
"keyboard_shortcuts.down": "Gluais sìos air an liosta",
"keyboard_shortcuts.enter": "Fosgail post",
"keyboard_shortcuts.favourite": "Cuir post ris na h-annsachdan",
"keyboard_shortcuts.favourites": "Fosgail liosta nan annsachdan",
"keyboard_shortcuts.federated": "Fosgail an loidhne-ama cho-naisgte",
"keyboard_shortcuts.heading": "Ath-ghoiridean a’ mheur-chlàir",
"keyboard_shortcuts.home": "a dh’fhosgladh loidhne-ama na dachaigh",
"keyboard_shortcuts.home": "Fosgail loidhne-ama na dachaigh",
"keyboard_shortcuts.hotkey": "Grad-iuchair",
"keyboard_shortcuts.legend": "a shealltainn a’ chlàir-mhìneachaidh seo",
"keyboard_shortcuts.local": "a dh’fhosgladh na loidhne-ama ionadail",
"keyboard_shortcuts.mention": "a thoirt iomradh dhan ùghdar",
"keyboard_shortcuts.muted": "a dh’fhosgladh liosta nan cleachdaichean mùchte",
"keyboard_shortcuts.my_profile": "a dh’fhosgladh na pròifil agad",
"keyboard_shortcuts.notifications": "a dh’fhosgladh colbh nam brathan",
"keyboard_shortcuts.open_media": "a dh’fhosgladh nam meadhanan",
"keyboard_shortcuts.pinned": "a dh’fhosgladh liosta na postaichean prìnichte",
"keyboard_shortcuts.profile": "a dh’fhosgladh pròifil an ùghdair",
"keyboard_shortcuts.reply": "a fhreagairt",
"keyboard_shortcuts.requests": "a dh’fhosgladh liosta nan iarrtasan leantainn",
"keyboard_shortcuts.search": "a chur am fòcas air a lorg",
"keyboard_shortcuts.spoilers": "a shealltainn/fhalach raon an rabhaidh susbainte",
"keyboard_shortcuts.start": "a dh’fhosgladh colbh “dèan toiseach-tòiseachaidh”",
"keyboard_shortcuts.toggle_hidden": "a shealltainn/fhalach teacsa fo rabhadh susbainte",
"keyboard_shortcuts.toggle_sensitivity": "a shealltainn/fhalach meadhanan",
"keyboard_shortcuts.toot": "a thòiseachadh air post ùr",
"keyboard_shortcuts.unfocus": "a thoirt am fòcas far raon teacsa an sgrìobhaidh/an luirg",
"keyboard_shortcuts.up": "a ghluasad suas air an liosta",
"keyboard_shortcuts.legend": "Seall an clàr-mìneachaidh seo",
"keyboard_shortcuts.local": "Fosgail an loidhne-ama ionadail",
"keyboard_shortcuts.mention": "Thoir iomradh dhan ùghdar",
"keyboard_shortcuts.muted": "Fosgail liosta nan cleachdaichean mùchte",
"keyboard_shortcuts.my_profile": "Fosgail a’ phròifil agad",
"keyboard_shortcuts.notifications": "Fosgail colbh nam brathan",
"keyboard_shortcuts.open_media": "Fosgail meadhan",
"keyboard_shortcuts.pinned": "Fosgail liosta nam postaichean prìnichte",
"keyboard_shortcuts.profile": "Fosgail pròifil an ùghdair",
"keyboard_shortcuts.reply": "Freagair do phost",
"keyboard_shortcuts.requests": "Fosgail liosta nan iarrtasan leantainn",
"keyboard_shortcuts.search": "Cuir am fòcas air a’ bhàr-luirg",
"keyboard_shortcuts.spoilers": "Seall/Falaich raon an rabhaidh susbainte",
"keyboard_shortcuts.start": "Fosgail an colbh “dèan toiseach-tòiseachaidh”",
"keyboard_shortcuts.toggle_hidden": "Seall/Falaich an teacsa fo rabhadh susbainte",
"keyboard_shortcuts.toggle_sensitivity": "Seall/Falaich na meadhanan",
"keyboard_shortcuts.toot": "Tòisich air post ùr",
"keyboard_shortcuts.unfocus": "Thoir am fòcas far raon teacsa an sgrìobhaidh/an luirg",
"keyboard_shortcuts.up": "Gluais suas air an liosta",
"lightbox.close": "Dùin",
"lightbox.compress": "Co-theannaich bogsa sealladh an deilbh",
"lightbox.expand": "Leudaich bogsa sealladh an deilbh",
@ -262,7 +262,7 @@
"lists.subheading": "Na liostaichean agad",
"load_pending": "{count, plural, one {# nì ùr} two {# nì ùr} few {# nithean ùra} other {# nì ùr}}",
"loading_indicator.label": "’Ga luchdadh…",
"media_gallery.toggle_visible": "Falaich {number, plural, 1 {an dealbh} one {na dealbhan} two {na dealbhan} few {na dealbhan} other {na dealbhan}}",
"media_gallery.toggle_visible": "{number, plural, 1 {Falaich an dealbh} one {Falaich na dealbhan} two {Falaich na dealbhan} few {Falaich na dealbhan} other {Falaich na dealbhan}}",
"missing_indicator.label": "Cha deach càil a lorg",
"missing_indicator.sublabel": "Cha deach an goireas a lorg",
"mute_modal.duration": "Faide",

2
app/javascript/mastodon/locales/hu.json

@ -51,7 +51,7 @@
"alert.rate_limited.title": "Forgalomkorlátozás",
"alert.unexpected.message": "Váratlan hiba történt.",
"alert.unexpected.title": "Hoppá!",
"announcement.announcement": "Bejelentés",
"announcement.announcement": "Közlemény",
"autosuggest_hashtag.per_week": "{count} hetente",
"boost_modal.combo": "Hogy átugord ezt következő alkalommal, használd {combo}",
"bundle_column_error.body": "Valami hiba történt a komponens betöltése közben.",

2
app/javascript/mastodon/locales/io.json

@ -142,7 +142,7 @@
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insertar emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",

2
app/javascript/mastodon/locales/it.json

@ -5,7 +5,7 @@
"account.badges.group": "Gruppo",
"account.block": "Blocca @{name}",
"account.block_domain": "Blocca dominio {domain}",
"account.blocked": "Bloccat*",
"account.blocked": "Bloccato",
"account.browse_more_on_origin_server": "Sfoglia ulteriormente sul profilo originale",
"account.cancel_follow_request": "Annulla richiesta di seguire",
"account.direct": "Messaggio diretto a @{name}",

2
app/javascript/mastodon/locales/kn.json

@ -142,7 +142,7 @@
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",

4
app/javascript/mastodon/locales/ko.json

@ -85,9 +85,9 @@
"community.column_settings.local_only": "로컬만",
"community.column_settings.media_only": "미디어만",
"community.column_settings.remote_only": "원격만",
"compose_form.direct_message_warning": "이 은 멘션 된 유저들에게만 보여집니다.",
"compose_form.direct_message_warning": "이 게시물은 멘션 된 유저들에게만 보여집니다.",
"compose_form.direct_message_warning_learn_more": "더 알아보기",
"compose_form.hashtag_warning": "이 툿은 어떤 해시태그로도 검색 되지 않습니다. 전체공개로 게시 된 툿만이 해시태그로 검색 될 수 있습니다.",
"compose_form.hashtag_warning": "이 게시물은 어떤 해시태그로도 검색 되지 않습니다. 전체공개로 게시 된 게시물만이 해시태그로 검색 될 수 있습니다.",
"compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
"compose_form.lock_disclaimer.lock": "비공개",
"compose_form.placeholder": "지금 무엇을 하고 있나요?",

36
app/javascript/mastodon/locales/lt.json

@ -1,8 +1,8 @@
{
"account.account_note_header": "Note",
"account.account_note_header": "Pastaba",
"account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot",
"account.badges.group": "Group",
"account.badges.group": "Grupė",
"account.block": "Block @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
@ -14,7 +14,7 @@
"account.edit_profile": "Edit profile",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile",
"account.follow": "Follow",
"account.follow": "Sekti",
"account.followers": "Followers",
"account.followers.empty": "No one follows this user yet.",
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
@ -31,8 +31,8 @@
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.never_active": "Never",
"account.muted": "Užtildytas",
"account.never_active": "Niekada",
"account.posts": "Toots",
"account.posts_with_replies": "Toots and replies",
"account.report": "Report @{name}",
@ -43,20 +43,20 @@
"account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unendorse": "Don't feature on profile",
"account.unfollow": "Unfollow",
"account.unfollow": "Nebesekti",
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.placeholder": "Click to add a note",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.",
"alert.unexpected.title": "Oops!",
"alert.unexpected.title": "Oi!",
"announcement.announcement": "Announcement",
"autosuggest_hashtag.per_week": "{count} per week",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_column_error.title": "Tinklo klaida",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
@ -66,22 +66,22 @@
"column.direct": "Direct messages",
"column.directory": "Browse profiles",
"column.domain_blocks": "Hidden domains",
"column.favourites": "Favourites",
"column.favourites": "Mėgstamiausi",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.lists": "Lists",
"column.mutes": "Muted users",
"column.lists": "Sąrašai",
"column.mutes": "Užtildyti vartotojai",
"column.notifications": "Notifications",
"column.pins": "Pinned toot",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_back_button.label": "Atgal",
"column_header.hide_settings": "Slėpti nustatymus",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.settings": "Settings",
"column_header.pin": "Prisegti",
"column_header.show_settings": "Rodyti nustatymus",
"column_header.unpin": "Atsegti",
"column_subheading.settings": "Nustatymai",
"community.column_settings.local_only": "Local only",
"community.column_settings.media_only": "Media only",
"community.column_settings.remote_only": "Remote only",
@ -142,7 +142,7 @@
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",

2
app/javascript/mastodon/locales/mk.json

@ -142,7 +142,7 @@
"emoji_button.food": "Храна &amp; Пијалаци",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Природа",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Објекти",
"emoji_button.people": "Луѓе",
"emoji_button.recent": "Најчесто користени",

12
app/javascript/mastodon/locales/ml.json

@ -22,7 +22,7 @@
"account.follows.empty": "ഈ ഉപയ ആര ഇതവരിടരില.",
"account.follows_you": "നിങളിടര",
"account.hide_reblogs": "@{name} ബതവ മറയക",
"account.joined": "Joined {date}",
"account.joined": "{date} ൽ ചർന",
"account.last_status": "അവസ കണടത",
"account.link_verified_on": "ഈ ലിി ഉടമസതത {date} ഇൽ ഉറപിയത",
"account.locked_info": "ഈ അഗതവതിവകയതിലപ അനസരിിടരനവരിരഞള വിചനി ഉടമസഥനിൽ നിിതമിി.",
@ -33,8 +33,8 @@
"account.mute_notifications": "@{name} യിൽ നിള അറിികൾ നിശബദമക",
"account.muted": "നിശബദമിിി",
"account.never_active": "ഒരികല ഇലല",
"account.posts": "കൾ",
"account.posts_with_replies": "കള മറപടികള",
"account.posts": "കൾ",
"account.posts_with_replies": "കള മറപടികള",
"account.report": "റിർടക @{name}",
"account.requested": "അനദതിിിി. പിടരള അപഷ റദൻ ഞക",
"account.share": "@{name} നൽ പങിക",
@ -75,7 +75,7 @@
"column.pins": "ഉറപിിിിിന ടകൾ",
"column.public": "സതമയ സമയരഖ",
"column_back_button.label": "പറകി",
"column_header.hide_settings": "സജകരണങങള മറയക",
"column_header.hide_settings": "രമകരണങങൾ മറയക",
"column_header.moveLeft_settings": "എഴി ഇടതക",
"column_header.moveRight_settings": "എഴി വലതക",
"column_header.pin": "ഉറപിിക",
@ -398,7 +398,7 @@
"status.mute": "@{name}-നിശബദമക",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.pin": "ിൽ പിൻ ച",
"status.pinned": "Pinned toot",
"status.read_more": "കതൽ വിക",
"status.reblog": "ബ",
@ -422,7 +422,7 @@
"status.unpin": "Unpin from profile",
"suggestions.dismiss": "നിർദ ഒഴി",
"suggestions.header": "നിങൾകൽപപരയമ…",
"tabs_bar.federated_timeline": "സതമിയ",
"tabs_bar.federated_timeline": "സതമയ",
"tabs_bar.home": "ഹ",
"tabs_bar.local_timeline": "പി",
"tabs_bar.notifications": "അറിികൾ",

2
app/javascript/mastodon/locales/mr.json

@ -142,7 +142,7 @@
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",

6
app/javascript/mastodon/locales/nl.json

@ -22,7 +22,7 @@
"account.follows.empty": "Deze gebruiker volgt nog niemand.",
"account.follows_you": "Volgt jou",
"account.hide_reblogs": "Boosts van @{name} verbergen",
"account.joined": "Joined {date}",
"account.joined": "Geregistreerd in {date}",
"account.last_status": "Laatst actief",
"account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}",
"account.locked_info": "De privacystatus van dit account is op besloten gezet. De eigenaar bepaalt handmatig wie hen kan volgen.",
@ -160,7 +160,7 @@
"empty_column.domain_blocks": "Er zijn nog geen geblokkeerde domeinen.",
"empty_column.favourited_statuses": "Jij hebt nog geen favoriete toots. Wanneer je er een aan jouw favorieten toevoegt, valt deze hier te zien.",
"empty_column.favourites": "Niemand heeft deze toot nog aan hun favorieten toegevoegd. Wanneer iemand dit doet, valt dat hier te zien.",
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"empty_column.follow_recommendations": "Het lijkt er op dat er geen aanbevelingen voor jou aangemaakt kunnen worden. Je kunt proberen te zoeken naar mensen die je wellicht kent, zoeken op hashtags, de lokale en globale tijdlijnen bekijken of de gebruikersgids doorbladeren.",
"empty_column.follow_requests": "Jij hebt nog enkel volgverzoek ontvangen. Wanneer je er eentje ontvangt, valt dat hier te zien.",
"empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
"empty_column.home": "Deze tijdlijn is leeg! Volg meer mensen om het te vullen. {suggestions}",
@ -267,7 +267,7 @@
"lists.subheading": "Jouw lijsten",
"load_pending": "{count, plural, one {# nieuw item} other {# nieuwe items}}",
"loading_indicator.label": "Laden…",
"media_gallery.toggle_visible": "Media verbergen",
"media_gallery.toggle_visible": "{number, plural, one {afbeelding verbergen} other {afbeeldingen verbergen}}",
"missing_indicator.label": "Niet gevonden",
"missing_indicator.sublabel": "Deze hulpbron kan niet gevonden worden",
"mute_modal.duration": "Duur",

2
app/javascript/mastodon/locales/pa.json

@ -142,7 +142,7 @@
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",

4
app/javascript/mastodon/locales/pt-PT.json

@ -1,5 +1,5 @@
{
"account.account_note_header": "A tua nota para @{name}",
"account.account_note_header": "Nota",
"account.add_or_remove_from_list": "Adicionar ou remover das listas",
"account.badges.bot": "Robô",
"account.badges.group": "Grupo",
@ -46,7 +46,7 @@
"account.unfollow": "Deixar de seguir",
"account.unmute": "Não silenciar @{name}",
"account.unmute_notifications": "Deixar de silenciar @{name}",
"account_note.placeholder": "Nenhum comentário fornecido",
"account_note.placeholder": "Clique para adicionar nota",
"alert.rate_limited.message": "Volte a tentar depois das {retry_time, time, medium}.",
"alert.rate_limited.title": "Limite de tentativas",
"alert.unexpected.message": "Ocorreu um erro inesperado.",

2
app/javascript/mastodon/locales/si.json

@ -142,7 +142,7 @@
"emoji_button.food": "ආහර සහ පන",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "No matching emojis found",
"emoji_button.objects": "Objects",
"emoji_button.people": "ම",
"emoji_button.recent": "නතර භ",

12
app/javascript/mastodon/locales/sv.json

@ -27,7 +27,7 @@
"account.link_verified_on": "Ägarskap för detta konto kontrollerades den {date}",
"account.locked_info": "Detta konto har låst integritetsstatus. Ägaren väljer manuellt vem som kan följa.",
"account.media": "Media",
"account.mention": "Nämna @{name}",
"account.mention": "Nämn @{name}",
"account.moved_to": "{name} har flyttat till:",
"account.mute": "Tysta @{name}",
"account.mute_notifications": "Stäng av notifieringar från @{name}",
@ -90,7 +90,7 @@
"compose_form.hashtag_warning": "Denna toot kommer inte att visas under någon hashtag eftersom den är onoterad. Endast offentliga toots kan sökas med hashtag.",
"compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vem som helst kan följa dig för att se dina inlägg som endast är för följare.",
"compose_form.lock_disclaimer.lock": "låst",
"compose_form.placeholder": "Vad funderar du på?",
"compose_form.placeholder": "Vad tänker du på?",
"compose_form.poll.add_option": "Lägg till ett val",
"compose_form.poll.duration": "Varaktighet för omröstning",
"compose_form.poll.option_placeholder": "Val {number}",
@ -105,7 +105,7 @@
"compose_form.spoiler.marked": "Texten är dold bakom en varning",
"compose_form.spoiler.unmarked": "Texten är inte dold",
"compose_form.spoiler_placeholder": "Skriv din varning här",
"confirmation_modal.cancel": "Ångra",
"confirmation_modal.cancel": "Avbryt",
"confirmations.block.block_and_report": "Blockera & rapportera",
"confirmations.block.confirm": "Blockera",
"confirmations.block.message": "Är du säker på att du vill blockera {name}?",
@ -128,7 +128,7 @@
"confirmations.unfollow.message": "Är du säker på att du vill avfölja {name}?",
"conversation.delete": "Radera konversation",
"conversation.mark_as_read": "Markera som läst",
"conversation.open": "Se konversation",
"conversation.open": "Visa konversation",
"conversation.with": "Med {names}",
"directory.federated": "Från känt servernätverk",
"directory.local": "Endast från {domain}",
@ -349,7 +349,7 @@
"privacy.change": "Justera sekretess",
"privacy.direct.long": "Skicka endast till nämnda användare",
"privacy.direct.short": "Direkt",
"privacy.private.long": "Skicka endast till följare",
"privacy.private.long": "Endast synligt för följare",
"privacy.private.short": "Endast följare",
"privacy.public.long": "Skicka till publik tidslinje",
"privacy.public.short": "Publik",
@ -463,7 +463,7 @@
"upload_modal.choose_image": "Välj bild",
"upload_modal.description_placeholder": "En snabb brun räv hoppar över den lata hunden",
"upload_modal.detect_text": "Upptäck bildens text",
"upload_modal.edit_media": "Redigera meida",
"upload_modal.edit_media": "Redigera media",
"upload_modal.hint": "Klicka eller dra cirkeln på förhandstitten för att välja den fokusering som alltid kommer synas på alla miniatyrer.",
"upload_modal.preparing_ocr": "Förbereder OCR…",
"upload_modal.preview_label": "Förhandstitt ({ratio})",

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save