Browse Source

Merge tag 'v3.4.5' into closed-social-v3

closed-social-v3
欧醚 2 years ago
parent
commit
bd03963e8c
149 changed files with 2750 additions and 2068 deletions
  1. +41
    -1
      .circleci/config.yml
  2. +6
    -0
      .env.production.sample
  3. +1
    -1
      .github/CODEOWNERS
  4. +34
    -0
      .github/workflows/build-image.yml
  5. +2
    -2
      AUTHORS.md
  6. +1402
    -1325
      CHANGELOG.md
  7. +2
    -2
      CONTRIBUTING.md
  8. +2
    -2
      Dockerfile
  9. +3
    -2
      Gemfile.lock
  10. +4
    -4
      README.md
  11. +2
    -2
      app.json
  12. +2
    -2
      app/controllers/activitypub/followers_synchronizations_controller.rb
  13. +9
    -1
      app/controllers/activitypub/outboxes_controller.rb
  14. +1
    -1
      app/controllers/admin/statuses_controller.rb
  15. +0
    -1
      app/controllers/auth/passwords_controller.rb
  16. +0
    -3
      app/controllers/auth/registrations_controller.rb
  17. +11
    -10
      app/controllers/auth/sessions_controller.rb
  18. +12
    -9
      app/controllers/concerns/sign_in_token_authentication_concern.rb
  19. +13
    -11
      app/controllers/concerns/two_factor_authentication_concern.rb
  20. +1
    -1
      app/controllers/follower_accounts_controller.rb
  21. +1
    -1
      app/controllers/following_accounts_controller.rb
  22. +2
    -1
      app/controllers/well_known/webfinger_controller.rb
  23. +3
    -3
      app/helpers/accounts_helper.rb
  24. +11
    -0
      app/helpers/application_helper.rb
  25. +14
    -7
      app/javascript/mastodon/actions/picture_in_picture.js
  26. +1
    -1
      app/javascript/mastodon/components/intersection_observer_article.js
  27. +40
    -0
      app/javascript/mastodon/components/modal_root.js
  28. +3
    -4
      app/javascript/mastodon/components/scrollable_list.js
  29. +2
    -2
      app/javascript/mastodon/components/status.js
  30. +2
    -3
      app/javascript/mastodon/components/status_list.js
  31. +2
    -4
      app/javascript/mastodon/containers/mastodon.js
  32. +18
    -0
      app/javascript/mastodon/containers/scroll_container.js
  33. +3
    -4
      app/javascript/mastodon/features/account_gallery/index.js
  34. +1
    -3
      app/javascript/mastodon/features/account_timeline/index.js
  35. +1
    -3
      app/javascript/mastodon/features/blocks/index.js
  36. +1
    -3
      app/javascript/mastodon/features/bookmarked_statuses/index.js
  37. +1
    -3
      app/javascript/mastodon/features/community_timeline/index.js
  38. +1
    -0
      app/javascript/mastodon/features/compose/containers/navigation_container.js
  39. +1
    -0
      app/javascript/mastodon/features/compose/index.js
  40. +0
    -1
      app/javascript/mastodon/features/direct_timeline/components/conversations_list.js
  41. +1
    -3
      app/javascript/mastodon/features/direct_timeline/index.js
  42. +3
    -4
      app/javascript/mastodon/features/directory/index.js
  43. +1
    -3
      app/javascript/mastodon/features/domain_blocks/index.js
  44. +1
    -3
      app/javascript/mastodon/features/favourited_statuses/index.js
  45. +1
    -3
      app/javascript/mastodon/features/favourites/index.js
  46. +1
    -3
      app/javascript/mastodon/features/follow_requests/index.js
  47. +1
    -3
      app/javascript/mastodon/features/followers/index.js
  48. +1
    -3
      app/javascript/mastodon/features/following/index.js
  49. +1
    -3
      app/javascript/mastodon/features/hashtag_timeline/index.js
  50. +1
    -3
      app/javascript/mastodon/features/home_timeline/index.js
  51. +1
    -3
      app/javascript/mastodon/features/list_timeline/index.js
  52. +1
    -2
      app/javascript/mastodon/features/lists/index.js
  53. +1
    -3
      app/javascript/mastodon/features/mutes/index.js
  54. +1
    -3
      app/javascript/mastodon/features/notifications/index.js
  55. +5
    -1
      app/javascript/mastodon/features/picture_in_picture/components/footer.js
  56. +1
    -3
      app/javascript/mastodon/features/pinned_statuses/index.js
  57. +1
    -3
      app/javascript/mastodon/features/public_timeline/index.js
  58. +1
    -3
      app/javascript/mastodon/features/reblogs/index.js
  59. +6
    -6
      app/javascript/mastodon/features/status/index.js
  60. +0
    -27
      app/javascript/mastodon/features/ui/components/audio_modal.js
  61. +8
    -1
      app/javascript/mastodon/features/ui/components/confirmation_modal.js
  62. +1
    -0
      app/javascript/mastodon/features/ui/components/link_footer.js
  63. +0
    -24
      app/javascript/mastodon/features/ui/components/media_modal.js
  64. +0
    -24
      app/javascript/mastodon/features/ui/components/video_modal.js
  65. +2
    -2
      app/javascript/mastodon/features/ui/containers/modal_container.js
  66. +26
    -32
      app/javascript/mastodon/features/ui/index.js
  67. +5
    -9
      app/javascript/mastodon/reducers/modal.js
  68. +3
    -0
      app/javascript/mastodon/reducers/picture_in_picture.js
  69. +1
    -0
      app/javascript/styles/mastodon/admin.scss
  70. +1
    -0
      app/javascript/styles/mastodon/components.scss
  71. +5
    -1
      app/lib/activitypub/activity/create.rb
  72. +15
    -11
      app/lib/activitypub/tag_manager.rb
  73. +1
    -30
      app/lib/formatter.rb
  74. +3
    -1
      app/lib/webfinger.rb
  75. +14
    -5
      app/models/account.rb
  76. +1
    -0
      app/models/account_note.rb
  77. +1
    -1
      app/models/canonical_email_block.rb
  78. +6
    -3
      app/models/concerns/account_interactions.rb
  79. +2
    -2
      app/models/status.rb
  80. +1
    -1
      app/models/user.rb
  81. +1
    -1
      app/serializers/manifest_serializer.rb
  82. +27
    -0
      app/serializers/rest/instance_serializer.rb
  83. +4
    -1
      app/services/fetch_oembed_service.rb
  84. +42
    -1
      app/services/notify_service.rb
  85. +3
    -0
      app/services/post_status_service.rb
  86. +1
    -0
      app/services/resolve_account_service.rb
  87. +1
    -1
      app/services/unsuspend_account_service.rb
  88. +2
    -1
      app/validators/status_length_validator.rb
  89. +2
    -2
      app/views/about/more.html.haml
  90. +2
    -2
      app/views/about/show.html.haml
  91. +5
    -5
      app/views/accounts/_header.html.haml
  92. +1
    -1
      app/views/accounts/show.html.haml
  93. +8
    -8
      app/views/admin/dashboard/index.html.haml
  94. +2
    -2
      app/views/admin/follow_recommendations/_account.html.haml
  95. +1
    -1
      app/views/admin/instances/_instance.html.haml
  96. +1
    -1
      app/views/admin/tags/_tag.html.haml
  97. +2
    -2
      app/views/directories/index.html.haml
  98. +2
    -2
      app/views/relationships/_account.html.haml
  99. +1
    -1
      app/views/settings/featured_tags/index.html.haml
  100. +3
    -3
      app/views/statuses/_detailed_status.html.haml

+ 41
- 1
.circleci/config.yml View File

@ -167,8 +167,45 @@ jobs:
name: Create database name: Create database
command: ./bin/rails db:create command: ./bin/rails db:create
- run: - 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 command: ./bin/rails db:migrate
name: Run all post-deployment remaining migrations
test-ruby2.7: test-ruby2.7:
<<: *defaults <<: *defaults
@ -238,6 +275,9 @@ workflows:
- test-migrations: - test-migrations:
requires: requires:
- install-ruby2.7 - install-ruby2.7
- test-two-step-migrations:
requires:
- install-ruby2.7
- test-ruby2.7: - test-ruby2.7:
requires: requires:
- install-ruby2.7 - install-ruby2.7

+ 6
- 0
.env.production.sample View File

@ -4,6 +4,12 @@
# not demonstrate all available configuration options. Please look at # not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation. # 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 # Federation
# ---------- # ----------
# This identifies your server and cannot be changed safely later # This identifies your server and cannot be changed safely later

+ 1
- 1
.github/CODEOWNERS View File

@ -1,4 +1,4 @@
# CODEOWNERS for tootsuite/mastodon
# CODEOWNERS for mastodon/mastodon
# Translators # 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. # 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
- 0
.github/workflows/build-image.yml View File

@ -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

+ 2
- 2
AUTHORS.md View File

@ -1,7 +1,7 @@
Authors 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: and provided thanks to the work of the following contributors:
* [Gargron](https://github.com/Gargron) * [Gargron](https://github.com/Gargron)
@ -719,7 +719,7 @@ and provided thanks to the work of the following contributors:
* [西小倉宏信](mailto:nishiko@mindia.jp) * [西小倉宏信](mailto:nishiko@mindia.jp)
* [雨宮美羽](mailto:k737566@gmail.com) * [雨宮美羽](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 ## Translators

+ 1402
- 1325
CHANGELOG.md
File diff suppressed because it is too large
View File


+ 2
- 2
CONTRIBUTING.md View File

@ -14,7 +14,7 @@ If your contributions are accepted into Mastodon, you can request to be paid thr
## Bug reports ## 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 ## Translations
@ -44,4 +44,4 @@ It is not always possible to phrase every change in such a manner, but it is des
## Documentation ## 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).

+ 2
- 2
Dockerfile View File

@ -54,8 +54,8 @@ RUN npm install -g yarn && \
COPY Gemfile* package.json yarn.lock /opt/mastodon/ COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /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)" && \ bundle install -j"$(nproc)" && \
yarn install --pure-lockfile yarn install --pure-lockfile

+ 3
- 2
Gemfile.lock View File

@ -545,8 +545,9 @@ GEM
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
ruby-progressbar (1.11.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) ruby2_keywords (0.0.4)
rufus-scheduler (3.6.0) rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)

+ 4
- 4
README.md View File

@ -1,14 +1,14 @@
![Mastodon](https://i.imgur.com/NhZc40l.png) ![Mastodon](https://i.imgur.com/NhZc40l.png)
======== ========
[![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
[![GitHub release](https://img.shields.io/github/release/mastodon/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/mastodon/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] [![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker] [![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
[releases]: https://github.com/tootsuite/mastodon/releases
[circleci]: https://circleci.com/gh/tootsuite/mastodon
[releases]: https://github.com/mastodon/mastodon/releases
[circleci]: https://circleci.com/gh/mastodon/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon
[crowdin]: https://crowdin.com/project/mastodon [crowdin]: https://crowdin.com/project/mastodon
[docker]: https://hub.docker.com/r/tootsuite/mastodon/ [docker]: https://hub.docker.com/r/tootsuite/mastodon/

+ 2
- 2
app.json View File

@ -1,8 +1,8 @@
{ {
"name": "Mastodon", "name": "Mastodon",
"description": "A GNU Social-compatible microblogging server", "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": { "env": {
"HEROKU": { "HEROKU": {
"description": "Leave this as true", "description": "Leave this as true",

+ 2
- 2
app/controllers/activitypub/followers_synchronizations_controller.rb View File

@ -19,11 +19,11 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
private private
def uri_prefix def uri_prefix
signed_request_account.uri[/http(s?):\/\/[^\/]+\//]
signed_request_account.uri[Account::URL_PREFIX_RE]
end end
def set_items 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 end
def collection_presenter def collection_presenter

+ 9
- 1
app/controllers/activitypub/outboxes_controller.rb View File

@ -11,7 +11,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
before_action :set_cache_headers before_action :set_cache_headers
def show 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' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
@ -76,4 +80,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def set_account def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end end
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
end
end end

+ 1
- 1
app/controllers/admin/statuses_controller.rb View File

@ -14,7 +14,7 @@ module Admin
@statuses = @account.statuses.where(visibility: [:public, :unlisted]) @statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media] 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 end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)

+ 0
- 1
app/controllers/auth/passwords_controller.rb View File

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

+ 0
- 3
app/controllers/auth/registrations_controller.rb View File

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

+ 11
- 10
app/controllers/auth/sessions_controller.rb View File

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

+ 12
- 9
app/controllers/concerns/sign_in_token_authentication_concern.rb View File

@ -16,21 +16,24 @@ module SignInTokenAuthenticationConcern
end end
def authenticate_with_sign_in_token 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
end end
def authenticate_with_sign_in_token_attempt(user) def authenticate_with_sign_in_token_attempt(user)
if valid_sign_in_token_attempt?(user) if valid_sign_in_token_attempt?(user)
clear_attempt_from_session clear_attempt_from_session
remember_me(user)
sign_in(user) sign_in(user)
else else
flash.now[:alert] = I18n.t('users.invalid_sign_in_token') flash.now[:alert] = I18n.t('users.invalid_sign_in_token')

+ 13
- 11
app/controllers/concerns/two_factor_authentication_concern.rb View File

@ -35,16 +35,20 @@ module TwoFactorAuthenticationConcern
end end
def authenticate_with_two_factor 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
end end
@ -53,7 +57,6 @@ module TwoFactorAuthenticationConcern
if valid_webauthn_credential?(user, webauthn_credential) if valid_webauthn_credential?(user, webauthn_credential)
clear_attempt_from_session clear_attempt_from_session
remember_me(user)
sign_in(user) sign_in(user)
render json: { redirect_path: root_path }, status: :ok render json: { redirect_path: root_path }, status: :ok
else else
@ -64,7 +67,6 @@ module TwoFactorAuthenticationConcern
def authenticate_with_two_factor_via_otp(user) def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user) if valid_otp_attempt?(user)
clear_attempt_from_session clear_attempt_from_session
remember_me(user)
sign_in(user) sign_in(user)
else else
flash.now[:alert] = I18n.t('users.invalid_otp_token') flash.now[:alert] = I18n.t('users.invalid_otp_token')

+ 1
- 1
app/controllers/follower_accounts_controller.rb View File

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

+ 1
- 1
app/controllers/following_accounts_controller.rb View File

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

+ 2
- 1
app/controllers/well_known/webfinger_controller.rb View File

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

+ 3
- 3
app/helpers/accounts_helper.rb View File

@ -80,17 +80,17 @@ module AccountsHelper
def account_description(account) def account_description(account)
prepend_str = [ 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), I18n.t('accounts.posts', count: account.statuses_count),
].join(' '), ].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), I18n.t('accounts.following', count: account.following_count),
].join(' '), ].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), I18n.t('accounts.followers', count: account.followers_count),
].join(' '), ].join(' '),
].join(', ') ].join(', ')

+ 11
- 0
app/helpers/application_helper.rb View File

@ -14,6 +14,17 @@ module ApplicationHelper
ku ku
).freeze ).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) def active_nav_class(*paths)
paths.any? { |path| current_page?(path) } ? 'active' : '' paths.any? { |path| current_page?(path) } ? 'active' : ''
end end

+ 14
- 7
app/javascript/mastodon/actions/picture_in_picture.js View File

@ -22,13 +22,20 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
* @param {MediaProps} props * @param {MediaProps} props
* @return {object} * @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} * @return {object}

+ 1
- 1
app/javascript/mastodon/components/intersection_observer_article.js View File

@ -93,7 +93,7 @@ export default class IntersectionObserverArticle extends React.Component {
// When the browser gets a chance, test if we're still not intersecting, // 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 // 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. // 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 })); this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
} }

+ 40
- 0
app/javascript/mastodon/components/modal_root.js View File

@ -1,10 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import 'wicg-inert'; import 'wicg-inert';
import { createBrowserHistory } from 'history';
import { multiply } from 'color-blend'; import { multiply } from 'color-blend';
export default class ModalRoot extends React.PureComponent { export default class ModalRoot extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
@ -48,6 +53,7 @@ export default class ModalRoot extends React.PureComponent {
componentDidMount () { componentDidMount () {
window.addEventListener('keyup', this.handleKeyUp, false); window.addEventListener('keyup', this.handleKeyUp, false);
window.addEventListener('keydown', this.handleKeyDown, false); window.addEventListener('keydown', this.handleKeyDown, false);
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
@ -69,6 +75,14 @@ export default class ModalRoot extends React.PureComponent {
this.activeElement.focus({ preventScroll: true }); this.activeElement.focus({ preventScroll: true });
this.activeElement = null; this.activeElement = null;
}).catch(console.error); }).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); 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 = () => { getSiblings = () => {
return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
} }

+ 3
- 4
app/javascript/mastodon/components/scrollable_list.js View File

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

+ 2
- 2
app/javascript/mastodon/components/status.js View File

@ -344,8 +344,8 @@ class Status extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'> <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> </div>
</HotKeys> </HotKeys>
); );

+ 2
- 3
app/javascript/mastodon/components/status_list.js View File

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

+ 2
- 4
app/javascript/mastodon/containers/mastodon.js View File

@ -10,8 +10,6 @@ import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming'; import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales'; 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 initialState from '../initial_state';
import ErrorBoundary from '../components/error_boundary'; 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 () { render () {

+ 18
- 0
app/javascript/mastodon/containers/scroll_container.js View File

@ -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,
};
}

+ 3
- 4
app/javascript/mastodon/features/account_gallery/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/account_timeline/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/blocks/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/bookmarked_statuses/index.js View File

@ -27,7 +27,6 @@ class Bookmarks extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired, statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
@ -68,7 +67,7 @@ class Bookmarks extends ImmutablePureComponent {
}, 300, { leading: true }) }, 300, { leading: true })
render () { render () {
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId; 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." />; 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} hasMore={hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
/> />

+ 1
- 3
app/javascript/mastodon/features/community_timeline/index.js View File

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

+ 1
- 0
app/javascript/mastodon/features/compose/containers/navigation_container.js View File

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

+ 1
- 0
app/javascript/mastodon/features/compose/index.js View File

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

+ 0
- 1
app/javascript/mastodon/features/direct_timeline/components/conversations_list.js View File

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

+ 1
- 3
app/javascript/mastodon/features/direct_timeline/index.js View File

@ -19,7 +19,6 @@ class DirectTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string, columnId: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
@ -71,7 +70,7 @@ class DirectTimeline extends React.PureComponent {
} }
render () { render () {
const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props;
const { intl, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -93,7 +92,6 @@ class DirectTimeline extends React.PureComponent {
timelineId='direct' timelineId='direct'
onLoadMore={this.handleLoadMore} 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." />} 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> </Column>
); );

+ 3
- 4
app/javascript/mastodon/features/directory/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/domain_blocks/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/favourited_statuses/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/favourites/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/follow_requests/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/followers/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/following/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/hashtag_timeline/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/home_timeline/index.js View File

@ -34,7 +34,6 @@ class HomeTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
isPartial: PropTypes.bool, isPartial: PropTypes.bool,
@ -112,7 +111,7 @@ class HomeTimeline extends React.PureComponent {
} }
render () { 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; const pinned = !!columnId;
let announcementsButton = null; let announcementsButton = null;
@ -154,7 +153,6 @@ class HomeTimeline extends React.PureComponent {
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
timelineId='home' 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> }} />} 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} bindToDocument={!multiColumn}
/> />
</Column> </Column>

+ 1
- 3
app/javascript/mastodon/features/list_timeline/index.js View File

@ -41,7 +41,6 @@ class ListTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string, columnId: PropTypes.string,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -142,7 +141,7 @@ class ListTimeline extends React.PureComponent {
} }
render () { render () {
const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list, intl } = this.props;
const { hasUnread, columnId, multiColumn, list, intl } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
const pinned = !!columnId; const pinned = !!columnId;
const title = list ? list.get('title') : id; const title = list ? list.get('title') : id;
@ -207,7 +206,6 @@ class ListTimeline extends React.PureComponent {
timelineId={`list:${id}`} timelineId={`list:${id}`}
onLoadMore={this.handleLoadMore} 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.' />} 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} bindToDocument={!multiColumn}
/> />
</Column> </Column>

+ 1
- 2
app/javascript/mastodon/features/lists/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/mutes/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/notifications/index.js View File

@ -74,7 +74,6 @@ class Notifications extends React.PureComponent {
notifications: ImmutablePropTypes.list.isRequired, notifications: ImmutablePropTypes.list.isRequired,
showFilterBar: PropTypes.bool.isRequired, showFilterBar: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
isUnread: PropTypes.bool, isUnread: PropTypes.bool,
@ -176,7 +175,7 @@ class Notifications extends React.PureComponent {
}; };
render () { 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 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." />; 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} onLoadPending={this.handleLoadPending}
onScrollToTop={this.handleScrollToTop} onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll} onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{scrollableContent} {scrollableContent}

+ 5
- 1
app/javascript/mastodon/features/picture_in_picture/components/footer.js View File

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

+ 1
- 3
app/javascript/mastodon/features/pinned_statuses/index.js View File

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

+ 1
- 3
app/javascript/mastodon/features/public_timeline/index.js View File

@ -43,7 +43,6 @@ class PublicTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -106,7 +105,7 @@ class PublicTimeline extends React.PureComponent {
} }
render () { render () {
const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -130,7 +129,6 @@ class PublicTimeline extends React.PureComponent {
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`} 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' />} 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} bindToDocument={!multiColumn}
/> />
</Column> </Column>

+ 1
- 3
app/javascript/mastodon/features/reblogs/index.js View File

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

+ 6
- 6
app/javascript/mastodon/features/status/index.js View File

@ -46,7 +46,7 @@ import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts'; import { initBoostModal } from '../../actions/boosts';
import { initReport } from '../../actions/reports'; import { initReport } from '../../actions/reports';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; 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 ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container'; import StatusContainer from '../../containers/status_container';
@ -84,7 +84,7 @@ const makeMapStateToProps = () => {
ancestorsIds = ancestorsIds.withMutations(mutable => { ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId; let id = statusId;
while (id) {
while (id && !mutable.includes(id)) {
mutable.unshift(id); mutable.unshift(id);
id = inReplyTos.get(id); id = inReplyTos.get(id);
} }
@ -102,7 +102,7 @@ const makeMapStateToProps = () => {
const ids = [statusId]; const ids = [statusId];
while (ids.length > 0) { while (ids.length > 0) {
let id = ids.shift();
let id = ids.pop();
const replies = contextReplies.get(id); const replies = contextReplies.get(id);
if (statusId !== id) { if (statusId !== id) {
@ -111,7 +111,7 @@ const makeMapStateToProps = () => {
if (replies) { if (replies) {
replies.reverse().forEach(reply => { replies.reverse().forEach(reply => {
ids.unshift(reply);
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
}); });
} }
} }
@ -559,7 +559,7 @@ class Status extends ImmutablePureComponent {
render () { render () {
let ancestors, descendants; let ancestors, descendants;
const { shouldUpdateScroll, status, deep, ancestorsIds, descendantsIds, treeData, intl, domain, multiColumn, pictureInPicture } = this.props;
const { status, deep, ancestorsIds, descendantsIds, treeData, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen, showTree, svgWidth, activeNode } = this.state; const { fullscreen, showTree, svgWidth, activeNode } = this.state;
if (status === null) { if (status === null) {
@ -602,7 +602,7 @@ class Status extends ImmutablePureComponent {
)} )}
/> />
<ScrollContainer scrollKey='thread' shouldUpdateScroll={shouldUpdateScroll}>
<ScrollContainer scrollKey='thread'>
<div className={classNames('scrollable', { fullscreen }, {'tree':deep!=null})} ref={this.setRef}> <div className={classNames('scrollable', { fullscreen }, {'tree':deep!=null})} ref={this.setRef}>
{ancestors} {ancestors}

+ 0
- 27
app/javascript/mastodon/features/ui/components/audio_modal.js View File

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio'; import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { previewState } from './video_modal';
import Footer from 'mastodon/features/picture_in_picture/components/footer'; import Footer from 'mastodon/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
@ -25,32 +24,6 @@ class AudioModal extends ImmutablePureComponent {
onChangeBackgroundColor: PropTypes.func.isRequired, 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 () { render () {
const { media, accountStaticAvatar, statusId, onClose } = this.props; const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};

+ 8
- 1
app/javascript/mastodon/features/ui/components/confirmation_modal.js View File

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

+ 1
- 0
app/javascript/mastodon/features/ui/components/link_footer.js View File

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

+ 0
- 24
app/javascript/mastodon/features/ui/components/media_modal.js View File

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

+ 0
- 24
app/javascript/mastodon/features/ui/components/video_modal.js View File

@ -6,8 +6,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import Footer from 'mastodon/features/picture_in_picture/components/footer'; import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash'; import { getAverageFromBlurhash } from 'mastodon/blurhash';
export const previewState = 'previewVideoModal';
export default class VideoModal extends ImmutablePureComponent { export default class VideoModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
@ -22,19 +20,9 @@ export default class VideoModal extends ImmutablePureComponent {
onChangeBackgroundColor: PropTypes.func.isRequired, onChangeBackgroundColor: PropTypes.func.isRequired,
}; };
static contextTypes = {
router: PropTypes.object,
};
componentDidMount () { componentDidMount () {
const { router } = this.context;
const { media, onChangeBackgroundColor, onClose } = this.props; 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')); const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
if (backgroundColor) { 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 () { render () {
const { media, statusId, onClose } = this.props; const { media, statusId, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};

+ 2
- 2
app/javascript/mastodon/features/ui/containers/modal_container.js View File

@ -3,8 +3,8 @@ import { closeModal } from '../../../actions/modal';
import ModalRoot from '../components/modal_root'; import ModalRoot from '../components/modal_root';
const mapStateToProps = state => ({ 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 => ({ const mapDispatchToProps = dispatch => ({

+ 26
- 32
app/javascript/mastodon/features/ui/index.js View File

@ -54,8 +54,6 @@ import {
FollowRecommendations, FollowRecommendations,
} from './util/async-components'; } from './util/async-components';
import { me } from '../../initial_state'; 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'; import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
// Dummy import, to make sure that <Status /> ends up in the application bundle. // 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 => { setRef = c => {
if (c) { if (c) {
this.node = c.getWrappedInstance(); this.node = c.getWrappedInstance();
@ -158,38 +152,38 @@ class SwitchingColumnsArea extends React.PureComponent {
{redirect} {redirect}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/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='/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='/start' component={FollowRecommendations} content={children} />
<WrappedRoute path='/search' component={Search} 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/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} /> <WrappedRoute component={GenericNotFound} content={children} />
</WrappedSwitch> </WrappedSwitch>

+ 5
- 9
app/javascript/mastodon/reducers/modal.js View File

@ -1,19 +1,15 @@
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE } from '../actions/timelines';
import { Stack as ImmutableStack, Map as ImmutableMap } from 'immutable';
const initialState = {
modalType: null,
modalProps: {},
};
export default function modal(state = initialState, action) {
export default function modal(state = ImmutableStack(), action) {
switch(action.type) { switch(action.type) {
case MODAL_OPEN: case MODAL_OPEN:
return { modalType: action.modalType, modalProps: action.modalProps };
return state.unshift(ImmutableMap({ modalType: action.modalType, modalProps: action.modalProps }));
case MODAL_CLOSE: case MODAL_CLOSE:
return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
return (action.modalType === undefined || action.modalType === state.getIn([0, 'modalType'])) ? state.shift() : state;
case TIMELINE_DELETE: case TIMELINE_DELETE:
return (state.modalProps.statusId === action.id) ? initialState : state;
return state.filterNot((modal) => modal.get('modalProps').statusId === action.id);
default: default:
return state; return state;
} }

+ 3
- 0
app/javascript/mastodon/reducers/picture_in_picture.js View File

@ -1,4 +1,5 @@
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture'; import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture';
import { TIMELINE_DELETE } from '../actions/timelines';
const initialState = { const initialState = {
statusId: null, statusId: null,
@ -16,6 +17,8 @@ export default function pictureInPicture(state = initialState, action) {
return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props }; return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props };
case PICTURE_IN_PICTURE_REMOVE: case PICTURE_IN_PICTURE_REMOVE:
return { ...initialState }; return { ...initialState };
case TIMELINE_DELETE:
return (state.statusId === action.id) ? { ...initialState } : state;
default: default:
return state; return state;
} }

+ 1
- 0
app/javascript/styles/mastodon/admin.scss View File

@ -829,6 +829,7 @@ a.name-tag,
padding: 0 5px; padding: 0 5px;
margin-bottom: 10px; margin-bottom: 10px;
flex: 1 0 50%; flex: 1 0 50%;
max-width: 100%;
} }
.account__header__fields, .account__header__fields,

+ 1
- 0
app/javascript/styles/mastodon/components.scss View File

@ -7284,6 +7284,7 @@ noscript {
&__account { &__account {
display: flex; display: flex;
text-decoration: none; text-decoration: none;
overflow: hidden;
} }
.account__avatar { .account__avatar {

+ 5
- 1
app/lib/activitypub/activity/create.rb View File

@ -446,10 +446,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
def supported_blurhash?(blurhash) def supported_blurhash?(blurhash)
components = blurhash.blank? ? nil : Blurhash.components(blurhash)
components = blurhash.blank? || !blurhash_valid_chars?(blurhash) ? nil : Blurhash.components(blurhash)
components.present? && components.none? { |comp| comp > 5 } components.present? && components.none? { |comp| comp > 5 }
end end
def blurhash_valid_chars?(blurhash)
/^[\w#$%*+-.:;=?@\[\]^{|}~]+$/.match?(blurhash)
end
def skip_download? def skip_download?
return @skip_download if defined?(@skip_download) return @skip_download if defined?(@skip_download)

+ 15
- 11
app/lib/activitypub/tag_manager.rb View File

@ -64,6 +64,10 @@ class ActivityPub::TagManager
account_status_replies_url(target.account, target, page_params) account_status_replies_url(target.account, target, page_params)
end end
def followers_uri_for(target)
target.local? ? account_followers_url(target) : target.followers_url.presence
end
# Primary audience of a status # Primary audience of a status
# Public statuses go out to primarily the public collection # Public statuses go out to primarily the public collection
# Unlisted and private statuses go out primarily to the followers collection # Unlisted and private statuses go out primarily to the followers collection
@ -80,17 +84,17 @@ class ActivityPub::TagManager
account_ids = status.active_mentions.pluck(:account_id) account_ids = status.active_mentions.pluck(:account_id)
to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result| to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
result << uri_for(account) result << uri_for(account)
result << account_followers_url(account) if account.group?
result << followers_uri_for(account) if account.group?
end end
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result| to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
result << uri_for(request.account) result << uri_for(request.account)
result << account_followers_url(request.account) if request.account.group?
end)
result << followers_uri_for(request.account) if request.account.group?
end).compact
else else
status.active_mentions.each_with_object([]) do |mention, result| status.active_mentions.each_with_object([]) do |mention, result|
result << uri_for(mention.account) result << uri_for(mention.account)
result << account_followers_url(mention.account) if mention.account.group?
end
result << followers_uri_for(mention.account) if mention.account.group?
end.compact
end end
end end
end end
@ -118,17 +122,17 @@ class ActivityPub::TagManager
account_ids = status.active_mentions.pluck(:account_id) account_ids = status.active_mentions.pluck(:account_id)
cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result| cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
result << uri_for(account) result << uri_for(account)
result << account_followers_url(account) if account.group?
end)
result << followers_uri_for(account) if account.group?
end.compact)
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result| cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
result << uri_for(request.account) result << uri_for(request.account)
result << account_followers_url(request.account) if request.account.group?
end)
result << followers_uri_for(request.account) if request.account.group?
end.compact)
else else
cc.concat(status.active_mentions.each_with_object([]) do |mention, result| cc.concat(status.active_mentions.each_with_object([]) do |mention, result|
result << uri_for(mention.account) result << uri_for(mention.account)
result << account_followers_url(mention.account) if mention.account.group?
end)
result << followers_uri_for(mention.account) if mention.account.group?
end.compact)
end end
end end

+ 1
- 30
app/lib/formatter.rb View File

@ -238,39 +238,10 @@ class Formatter
result.flatten.join result.flatten.join
end end
UNICODE_ESCAPE_BLACKLIST_RE = /\p{Z}|\p{P}/
def utf8_friendly_extractor(text, options = {}) def utf8_friendly_extractor(text, options = {})
old_to_new_index = [0]
escaped = text.chars.map do |c|
output = begin
if c.ord.to_s(16).length > 2 && !UNICODE_ESCAPE_BLACKLIST_RE.match?(c)
CGI.escape(c)
else
c
end
end
old_to_new_index << old_to_new_index.last + output.length
output
end.join
# Note: I couldn't obtain list_slug with @user/list-name format # Note: I couldn't obtain list_slug with @user/list-name format
# for mention so this requires additional check # for mention so this requires additional check
special = Extractor.extract_urls_with_indices(escaped, options).map do |extract|
new_indices = [
old_to_new_index.find_index(extract[:indices].first),
old_to_new_index.find_index(extract[:indices].last),
]
next extract.merge(
indices: new_indices,
url: text[new_indices.first..new_indices.last - 1]
)
end
special = Extractor.extract_urls_with_indices(text, options)
standard = Extractor.extract_entities_with_indices(text, options) standard = Extractor.extract_entities_with_indices(text, options)
extra = Extractor.extract_extra_uris_with_indices(text, options) extra = Extractor.extract_extra_uris_with_indices(text, options)

+ 3
- 1
app/lib/webfinger.rb View File

@ -46,7 +46,9 @@ class Webfinger
def body_from_webfinger(url = standard_url, use_fallback = true) def body_from_webfinger(url = standard_url, use_fallback = true)
webfinger_request(url).perform do |res| webfinger_request(url).perform do |res|
if res.code == 200 if res.code == 200
res.body_with_limit
body = res.body_with_limit
raise Webfinger::Error, "Request for #{@uri} returned empty response" if body.empty?
body
elsif res.code == 404 && use_fallback elsif res.code == 404 && use_fallback
body_from_host_meta body_from_host_meta
elsif res.code == 410 elsif res.code == 410

+ 14
- 5
app/models/account.rb View File

@ -58,8 +58,9 @@ class Account < ApplicationRecord
hub_url hub_url
) )
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
include AccountAssociations include AccountAssociations
include AccountAvatar include AccountAvatar
@ -295,7 +296,11 @@ class Account < ApplicationRecord
end end
def fields def fields
(self[:fields] || []).map { |f| Field.new(self, f) }
(self[:fields] || []).map do |f|
Field.new(self, f)
rescue
nil
end.compact
end end
def fields_attributes=(attributes) def fields_attributes=(attributes)
@ -375,7 +380,7 @@ class Account < ApplicationRecord
def synchronization_uri_prefix def synchronization_uri_prefix
return 'local' if local? return 'local' if local?
@synchronization_uri_prefix ||= uri[/http(s?):\/\/[^\/]+\//]
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
end end
class Field < ActiveModelSerializers::Model class Field < ActiveModelSerializers::Model
@ -570,7 +575,11 @@ class Account < ApplicationRecord
def create_canonical_email_block! def create_canonical_email_block!
return unless local? && user_email.present? return unless local? && user_email.present?
CanonicalEmailBlock.create(reference_account: self, email: user_email)
begin
CanonicalEmailBlock.create(reference_account: self, email: user_email)
rescue ActiveRecord::RecordNotUnique
# A canonical e-mail block may already exist for the same e-mail
end
end end
def destroy_canonical_email_block! def destroy_canonical_email_block!

+ 1
- 0
app/models/account_note.rb View File

@ -17,4 +17,5 @@ class AccountNote < ApplicationRecord
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
validates :comment, length: { maximum: 2_000 }
end end

+ 1
- 1
app/models/canonical_email_block.rb View File

@ -15,7 +15,7 @@ class CanonicalEmailBlock < ApplicationRecord
belongs_to :reference_account, class_name: 'Account' belongs_to :reference_account, class_name: 'Account'
validates :canonical_email_hash, presence: true
validates :canonical_email_hash, presence: true, uniqueness: true
def email=(email) def email=(email)
self.canonical_email_hash = email_to_canonical_email_hash(email) self.canonical_email_hash = email_to_canonical_email_hash(email)

+ 6
- 3
app/models/concerns/account_interactions.rb View File

@ -251,10 +251,13 @@ module AccountInteractions
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago) .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
end end
def remote_followers_hash(url_prefix)
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}") do
def remote_followers_hash(url)
url_prefix = url[Account::URL_PREFIX_RE]
return if url_prefix.blank?
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do
digest = "\x00" * 32 digest = "\x00" * 32
followers.where(Account.arel_table[:uri].matches(url_prefix + '%', false, true)).pluck_each(:uri) do |uri|
followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%";, false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri|
Xorcist.xor!(digest, Digest::SHA256.digest(uri)) Xorcist.xor!(digest, Digest::SHA256.digest(uri))
end end
digest.unpack('H*')[0] digest.unpack('H*')[0]

+ 2
- 2
app/models/status.rb View File

@ -339,7 +339,7 @@ class Status < ApplicationRecord
def from_text(text) def from_text(text)
return [] if text.blank? return [] if text.blank?
text.scan(FetchLinkCardService::URL_PATTERN).map(&:first).uniq.filter_map do |url|
text.scan(FetchLinkCardService::URL_PATTERN).map(&:second).uniq.filter_map do |url|
status = begin status = begin
if TagManager.instance.local_url?(url) if TagManager.instance.local_url?(url)
ActivityPub::TagManager.instance.uri_to_resource(url, Status) ActivityPub::TagManager.instance.uri_to_resource(url, Status)
@ -427,7 +427,7 @@ class Status < ApplicationRecord
end end
def decrement_counter_caches def decrement_counter_caches
return if direct_visibility?
return if direct_visibility? || new_record?
account&.decrement_count!(:statuses_count) account&.decrement_count!(:statuses_count)
reblog&.decrement_count!(:reblogs_count) if reblog? reblog&.decrement_count!(:reblogs_count) if reblog?

+ 1
- 1
app/models/user.rb View File

@ -63,7 +63,7 @@ class User < ApplicationRecord
devise :two_factor_backupable, devise :two_factor_backupable,
otp_number_of_backup_codes: 10 otp_number_of_backup_codes: 10
devise :registerable, :recoverable, :rememberable, :validatable,
devise :registerable, :recoverable, :validatable,
:confirmable :confirmable
include Omniauthable include Omniauthable

+ 1
- 1
app/serializers/manifest_serializer.rb View File

@ -48,7 +48,7 @@ class ManifestSerializer < ActiveModel::Serializer
end end
def scope def scope
root_url
'/'
end end
def share_target def share_target

+ 27
- 0
app/serializers/rest/instance_serializer.rb View File

@ -6,6 +6,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :uri, :title, :short_description, :description, :email, attributes :uri, :title, :short_description, :description, :email,
:version, :urls, :stats, :thumbnail, :version, :urls, :stats, :thumbnail,
:languages, :registrations, :approval_required, :invites_enabled, :languages, :registrations, :approval_required, :invites_enabled,
:configuration,
:max_toot_chars, :poll_limits :max_toot_chars, :poll_limits
has_one :contact_account, serializer: REST::AccountSerializer has_one :contact_account, serializer: REST::AccountSerializer
@ -54,6 +55,32 @@ class REST::InstanceSerializer < ActiveModel::Serializer
{ streaming_api: Rails.configuration.x.streaming_api_base_url } { streaming_api: Rails.configuration.x.streaming_api_base_url }
end end
def configuration
{
statuses: {
max_characters: StatusLengthValidator::MAX_CHARS,
max_media_attachments: 4,
characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS,
},
media_attachments: {
supported_mime_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES,
image_size_limit: MediaAttachment::IMAGE_LIMIT,
image_matrix_limit: Attachmentable::MAX_MATRIX_LIMIT,
video_size_limit: MediaAttachment::VIDEO_LIMIT,
video_frame_rate_limit: MediaAttachment::MAX_VIDEO_FRAME_RATE,
video_matrix_limit: MediaAttachment::MAX_VIDEO_MATRIX_LIMIT,
},
polls: {
max_options: PollValidator::MAX_OPTIONS,
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
min_expiration: PollValidator::MIN_EXPIRATION,
max_expiration: PollValidator::MAX_EXPIRATION,
},
}
end
def languages def languages
[I18n.default_locale] [I18n.default_locale]
end end

+ 4
- 1
app/services/fetch_oembed_service.rb View File

@ -2,6 +2,7 @@
class FetchOEmbedService class FetchOEmbedService
ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
URL_REGEX = /(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i.freeze
attr_reader :url, :options, :format, :endpoint_url attr_reader :url, :options, :format, :endpoint_url
@ -65,10 +66,12 @@ class FetchOEmbedService
end end
def cache_endpoint! def cache_endpoint!
return unless URL_REGEX.match?(@endpoint_url)
url_domain = Addressable::URI.parse(@url).normalized_host url_domain = Addressable::URI.parse(@url).normalized_host
endpoint_hash = { endpoint_hash = {
endpoint: @endpoint_url.gsub(/(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i, '={url}'),
endpoint: @endpoint_url.gsub(URL_REGEX, '={url}'),
format: @format, format: @format,
} }

+ 42
- 1
app/services/notify_service.rb View File

@ -67,8 +67,49 @@ class NotifyService < BaseService
message? && @notification.target_status.direct_visibility? message? && @notification.target_status.direct_visibility?
end end
# Returns true if the sender has been mentionned by the recipient up the thread
def response_to_recipient? def response_to_recipient?
@notification.target_status.in_reply_to_account_id == @recipient.id && @notification.target_status.thread&.direct_visibility?
return false if @notification.target_status.in_reply_to_id.nil?
# Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads
!Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id]).zero?
WITH RECURSIVE ancestors(id, in_reply_to_id, replying_to_sender) AS (
SELECT
s.id, s.in_reply_to_id, (CASE
WHEN s.account_id = :recipient_id THEN
EXISTS (
SELECT *
FROM mentions m
WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
)
ELSE
FALSE
END)
FROM statuses s
WHERE s.id = :id
UNION ALL
SELECT
s.id,
s.in_reply_to_id,
(CASE
WHEN s.account_id = :recipient_id THEN
EXISTS (
SELECT *
FROM mentions m
WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
)
ELSE
FALSE
END)
FROM ancestors st
JOIN statuses s ON s.id = st.in_reply_to_id
WHERE st.replying_to_sender IS FALSE
)
SELECT COUNT(*)
FROM ancestors st
JOIN statuses s ON s.id = st.id
WHERE st.replying_to_sender IS TRUE AND s.visibility = 3
SQL
end end
def from_staff? def from_staff?

+ 3
- 0
app/services/post_status_service.rb View File

@ -74,6 +74,9 @@ class PostStatusService < BaseService
status_for_validation = @account.statuses.build(status_attributes) status_for_validation = @account.statuses.build(status_attributes)
if status_for_validation.valid? if status_for_validation.valid?
# Marking the status as destroyed is necessary to prevent the status from being
# persisted when the associated media attachments get updated when creating the
# scheduled status.
status_for_validation.destroy status_for_validation.destroy
# The following transaction block is needed to wrap the UPDATEs to # The following transaction block is needed to wrap the UPDATEs to

+ 1
- 0
app/services/resolve_account_service.rb View File

@ -142,6 +142,7 @@ class ResolveAccountService < BaseService
end end
def queue_deletion! def queue_deletion!
@account.suspend!(origin: :remote)
AccountDeletionWorker.perform_async(@account.id, reserve_username: false, skip_activitypub: true) AccountDeletionWorker.perform_async(@account.id, reserve_username: false, skip_activitypub: true)
end end

+ 1
- 1
app/services/unsuspend_account_service.rb View File

@ -7,7 +7,7 @@ class UnsuspendAccountService < BaseService
unsuspend! unsuspend!
refresh_remote_account! refresh_remote_account!
return if @account.nil?
return if @account.nil? || @account.suspended?
merge_into_home_timelines! merge_into_home_timelines!
merge_into_list_timelines! merge_into_list_timelines!

+ 2
- 1
app/validators/status_length_validator.rb View File

@ -2,7 +2,8 @@
class StatusLengthValidator < ActiveModel::Validator class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 5000 MAX_CHARS = 5000
URL_PLACEHOLDER = "\1#{'x' * 23}"
URL_PLACEHOLDER_CHARS = 23
URL_PLACEHOLDER = "\1#{'x' * URL_PLACEHOLDER_CHARS}"
def validate(status) def validate(status)
return unless status.local? && !status.reblog? return unless status.local? && !status.reblog?

+ 2
- 2
app/views/about/more.html.haml View File

@ -17,11 +17,11 @@
.row__information-board .row__information-board
.information-board__section .information-board__section
%span= t 'about.user_count_before' %span= t 'about.user_count_before'
%strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
%strong= friendly_number_to_human @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count %span= t 'about.user_count_after', count: @instance_presenter.user_count
.information-board__section .information-board__section
%span= t 'about.status_count_before' %span= t 'about.status_count_before'
%strong= number_to_human @instance_presenter.status_count, strip_insignificant_zeros: true
%strong= friendly_number_to_human @instance_presenter.status_count
%span= t 'about.status_count_after', count: @instance_presenter.status_count %span= t 'about.status_count_after', count: @instance_presenter.status_count
.row__mascot .row__mascot
.landing-page__mascot .landing-page__mascot

+ 2
- 2
app/views/about/show.html.haml View File

@ -67,10 +67,10 @@
.hero-widget__counters__wrapper .hero-widget__counters__wrapper
.hero-widget__counter .hero-widget__counter
%strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
%strong= friendly_number_to_human @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count %span= t 'about.user_count_after', count: @instance_presenter.user_count
.hero-widget__counter .hero-widget__counter
%strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
%strong= friendly_number_to_human @instance_presenter.active_user_count
%span %span
= t 'about.active_count_after' = t 'about.active_count_after'
%abbr{ title: t('about.active_footnote') } * %abbr{ title: t('about.active_footnote') } *

+ 5
- 5
app/views/accounts/_header.html.haml View File

@ -15,17 +15,17 @@
.details-counters .details-counters
.counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) } .counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) }
= link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do
%span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
%span.counter-number= friendly_number_to_human account.statuses_count
%span.counter-label= t('accounts.posts', count: account.statuses_count) %span.counter-label= t('accounts.posts', count: account.statuses_count)
.counter{ class: active_nav_class(account_following_index_url(account)) } .counter{ class: active_nav_class(account_following_index_url(account)) }
= link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do = link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do
%span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true
%span.counter-number= friendly_number_to_human account.following_count
%span.counter-label= t('accounts.following', count: account.following_count) %span.counter-label= t('accounts.following', count: account.following_count)
.counter{ class: active_nav_class(account_followers_url(account)) } .counter{ class: active_nav_class(account_followers_url(account)) }
= link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do = link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do
%span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true
%span.counter-number= friendly_number_to_human account.followers_count
%span.counter-label= t('accounts.followers', count: account.followers_count) %span.counter-label= t('accounts.followers', count: account.followers_count)
.spacer .spacer
.public-account-header__tabs__tabs__buttons .public-account-header__tabs__tabs__buttons
@ -36,8 +36,8 @@
.public-account-header__extra__links .public-account-header__extra__links
= link_to account_following_index_url(account) do = link_to account_following_index_url(account) do
%strong= number_to_human account.following_count, strip_insignificant_zeros: true
%strong= friendly_number_to_human account.following_count
= t('accounts.following', count: account.following_count) = t('accounts.following', count: account.following_count)
= link_to account_followers_url(account) do = link_to account_followers_url(account) do
%strong= number_to_human account.followers_count, strip_insignificant_zeros: true
%strong= friendly_number_to_human account.followers_count
= t('accounts.followers', count: account.followers_count) = t('accounts.followers', count: account.followers_count)

+ 1
- 1
app/views/accounts/show.html.haml View File

@ -81,6 +81,6 @@
= t('accounts.nothing_here') = t('accounts.nothing_here')
- else - else
%time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
.trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
.trends__item__current= friendly_number_to_human featured_tag.statuses_count
= render 'application/sidebar' = render 'application/sidebar'

+ 8
- 8
app/views/admin/dashboard/index.html.haml View File

@ -13,42 +13,42 @@
%div %div
= link_to admin_accounts_url(local: 1, recent: 1) do = link_to admin_accounts_url(local: 1, recent: 1) do
.dashboard__counters__num{ title: number_with_delimiter(@users_count, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@users_count, strip_insignificant_zeros: true) }
= number_to_human @users_count, strip_insignificant_zeros: true
= friendly_number_to_human @users_count
.dashboard__counters__label= t 'admin.dashboard.total_users' .dashboard__counters__label= t 'admin.dashboard.total_users'
%div %div
%div %div
.dashboard__counters__num{ title: number_with_delimiter(@registrations_week, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@registrations_week, strip_insignificant_zeros: true) }
= number_to_human @registrations_week, strip_insignificant_zeros: true
= friendly_number_to_human @registrations_week
.dashboard__counters__label= t 'admin.dashboard.week_users_new' .dashboard__counters__label= t 'admin.dashboard.week_users_new'
%div %div
%div %div
.dashboard__counters__num{ title: number_with_delimiter(@logins_week, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@logins_week, strip_insignificant_zeros: true) }
= number_to_human @logins_week, strip_insignificant_zeros: true
= friendly_number_to_human @logins_week
.dashboard__counters__label= t 'admin.dashboard.week_users_active' .dashboard__counters__label= t 'admin.dashboard.week_users_active'
%div %div
= link_to admin_pending_accounts_path do = link_to admin_pending_accounts_path do
.dashboard__counters__num{ title: number_with_delimiter(@pending_users_count, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@pending_users_count, strip_insignificant_zeros: true) }
= number_to_human @pending_users_count, strip_insignificant_zeros: true
= friendly_number_to_human @pending_users_count
.dashboard__counters__label= t 'admin.dashboard.pending_users' .dashboard__counters__label= t 'admin.dashboard.pending_users'
%div %div
= link_to admin_reports_url do = link_to admin_reports_url do
.dashboard__counters__num{ title: number_with_delimiter(@reports_count, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@reports_count, strip_insignificant_zeros: true) }
= number_to_human @reports_count, strip_insignificant_zeros: true
= friendly_number_to_human @reports_count
.dashboard__counters__label= t 'admin.dashboard.open_reports' .dashboard__counters__label= t 'admin.dashboard.open_reports'
%div %div
= link_to admin_tags_path(pending_review: '1') do = link_to admin_tags_path(pending_review: '1') do
.dashboard__counters__num{ title: number_with_delimiter(@pending_tags_count, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@pending_tags_count, strip_insignificant_zeros: true) }
= number_to_human @pending_tags_count, strip_insignificant_zeros: true
= friendly_number_to_human @pending_tags_count
.dashboard__counters__label= t 'admin.dashboard.pending_tags' .dashboard__counters__label= t 'admin.dashboard.pending_tags'
%div %div
%div %div
.dashboard__counters__num{ title: number_with_delimiter(@interactions_week, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@interactions_week, strip_insignificant_zeros: true) }
= number_to_human @interactions_week, strip_insignificant_zeros: true
= friendly_number_to_human @interactions_week
.dashboard__counters__label= t 'admin.dashboard.week_interactions' .dashboard__counters__label= t 'admin.dashboard.week_interactions'
%div %div
= link_to sidekiq_url do = link_to sidekiq_url do
.dashboard__counters__num{ title: number_with_delimiter(@queue_backlog, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@queue_backlog, strip_insignificant_zeros: true) }
= number_to_human @queue_backlog, strip_insignificant_zeros: true
= friendly_number_to_human @queue_backlog
.dashboard__counters__label= t 'admin.dashboard.backlog' .dashboard__counters__label= t 'admin.dashboard.backlog'
.dashboard__widgets .dashboard__widgets

+ 2
- 2
app/views/admin/follow_recommendations/_account.html.haml View File

@ -7,10 +7,10 @@
%tr %tr
%td= account_link_to account %td= account_link_to account
%td.accounts-table__count.optional %td.accounts-table__count.optional
= number_to_human account.statuses_count, strip_insignificant_zeros: true
= friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase %small= t('accounts.posts', count: account.statuses_count).downcase
%td.accounts-table__count.optional %td.accounts-table__count.optional
= number_to_human account.followers_count, strip_insignificant_zeros: true
= friendly_number_to_human account.followers_count
%small= t('accounts.followers', count: account.followers_count).downcase %small= t('accounts.followers', count: account.followers_count).downcase
%td.accounts-table__count %td.accounts-table__count
- if account.last_status_at.present? - if account.last_status_at.present?

+ 1
- 1
app/views/admin/instances/_instance.html.haml View File

@ -30,4 +30,4 @@
= ' / ' = ' / '
%span.negative-hint %span.negative-hint
= t('admin.instances.delivery.unavailable_message') = t('admin.instances.delivery.unavailable_message')
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= friendly_number_to_human instance.accounts_count

+ 1
- 1
app/views/admin/tags/_tag.html.haml View File

@ -16,4 +16,4 @@
= fa_icon 'fire fw' = fa_icon 'fire fw'
= t('admin.tags.trending_right_now') = t('admin.tags.trending_right_now')
.trends__item__current= number_to_human tag.history.first[:uses], strip_insignificant_zeros: true
.trends__item__current= friendly_number_to_human tag.history.first[:uses]

+ 2
- 2
app/views/directories/index.html.haml View File

@ -39,10 +39,10 @@
.directory__card__extra .directory__card__extra
.accounts-table__count .accounts-table__count
= number_to_human account.statuses_count, strip_insignificant_zeros: true
= friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase %small= t('accounts.posts', count: account.statuses_count).downcase
.accounts-table__count .accounts-table__count
= number_to_human account.followers_count, strip_insignificant_zeros: true
= friendly_number_to_human account.followers_count
%small= t('accounts.followers', count: account.followers_count).downcase %small= t('accounts.followers', count: account.followers_count).downcase
.accounts-table__count .accounts-table__count
- if account.last_status_at.present? - if account.last_status_at.present?

+ 2
- 2
app/views/relationships/_account.html.haml View File

@ -9,10 +9,10 @@
= interrelationships_icon(@relationships, account.id) = interrelationships_icon(@relationships, account.id)
%td= account_link_to account %td= account_link_to account
%td.accounts-table__count.optional %td.accounts-table__count.optional
= number_to_human account.statuses_count, strip_insignificant_zeros: true
= friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase %small= t('accounts.posts', count: account.statuses_count).downcase
%td.accounts-table__count.optional %td.accounts-table__count.optional
= number_to_human account.followers_count, strip_insignificant_zeros: true
= friendly_number_to_human account.followers_count
%small= t('accounts.followers', count: account.followers_count).downcase %small= t('accounts.followers', count: account.followers_count).downcase
%td.accounts-table__count %td.accounts-table__count
- if account.last_status_at.present? - if account.last_status_at.present?

+ 1
- 1
app/views/settings/featured_tags/index.html.haml View File

@ -28,4 +28,4 @@
- else - else
%time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
= table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
.trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
.trends__item__current= friendly_number_to_human featured_tag.statuses_count

+ 3
- 3
app/views/statuses/_detailed_status.html.haml View File

@ -55,18 +55,18 @@
= fa_icon('comment') = fa_icon('comment')
- else - else
= fa_icon('comments') = fa_icon('comments')
%span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true
%span.detailed-status__reblogs>= friendly_number_to_human status.replies_count
= " " = " "
· ·
- if status.public_visibility? || status.unlisted_visibility? - if status.public_visibility? || status.unlisted_visibility?
= link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
= fa_icon('retweet') = fa_icon('retweet')
%span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true
%span.detailed-status__reblogs>= friendly_number_to_human status.reblogs_count
= " " = " "
· ·
= link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
= fa_icon('heart') = fa_icon('heart')
%span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true
%span.detailed-status__favorites>= friendly_number_to_human status.favourites_count
= " " = " "
- if user_signed_in? - if user_signed_in?

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

Loading…
Cancel
Save