Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233c3e4e70 | ||
|
|
06433a5152 | ||
|
|
daaeabed21 | ||
|
|
1fb4443381 | ||
|
|
3873836f69 | ||
|
|
036760cc51 | ||
|
|
eca7b6538a | ||
|
|
480c3057d4 | ||
|
|
f8d0acd36e | ||
|
|
a04682c12b | ||
|
|
60c5249bdc | ||
|
|
2de4a25862 | ||
|
|
a55f46f16b |
No files matched your search
@@ -70,7 +70,7 @@ services:
|
||||
hard: -1
|
||||
|
||||
libretranslate:
|
||||
image: libretranslate/libretranslate:v1.3.12
|
||||
image: libretranslate/libretranslate:v1.3.11
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- lt-data:/home/libretranslate/.local
|
||||
|
||||
+1
-2
@@ -9,6 +9,7 @@ module.exports = {
|
||||
'plugin:import/recommended',
|
||||
'plugin:promise/recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
|
||||
env: {
|
||||
@@ -62,9 +63,7 @@ module.exports = {
|
||||
'consistent-return': 'error',
|
||||
'dot-notation': 'error',
|
||||
eqeqeq: ['error', 'always', { 'null': 'ignore' }],
|
||||
'indent': ['error', 2],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
'semi': ['error', 'always'],
|
||||
'no-case-declarations': 'off',
|
||||
'no-catch-shadow': 'error',
|
||||
'no-console': [
|
||||
|
||||
@@ -29,10 +29,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
id: buildx
|
||||
if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }}
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
run: |
|
||||
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
id: buildx-native
|
||||
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
||||
with:
|
||||
@@ -61,20 +61,20 @@ jobs:
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: contains(inputs.push_to_images, 'tootsuite')
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Github Container registry
|
||||
if: contains(inputs.push_to_images, 'ghcr.io')
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/metadata-action@v5
|
||||
- uses: docker/metadata-action@v4
|
||||
id: meta
|
||||
if: ${{ inputs.push_to_images != '' }}
|
||||
with:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
tags: ${{ inputs.tags }}
|
||||
labels: ${{ inputs.labels }}
|
||||
|
||||
- uses: docker/build-push-action@v5
|
||||
- uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
|
||||
@@ -282,8 +282,8 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
search:
|
||||
image: ${{ matrix.search-image }}
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
||||
env:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
@@ -313,11 +313,6 @@ jobs:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
search-image:
|
||||
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
||||
include:
|
||||
- ruby-version: '.ruby-version'
|
||||
search-image: docker.elastic.co/elasticsearch/elasticsearch:8.10.2
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
# Ignore Vagrant files
|
||||
.vagrant/
|
||||
|
||||
# Ignore Capistrano customizations
|
||||
/config/deploy/*
|
||||
|
||||
# Ignore IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
+11
-5
@@ -1,13 +1,13 @@
|
||||
# This configuration was generated by
|
||||
# `haml-lint --auto-gen-config`
|
||||
# on 2023-10-03 08:32:28 -0400 using Haml-Lint version 0.51.0.
|
||||
# on 2023-07-20 09:47:50 -0400 using Haml-Lint version 0.48.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the lints are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of Haml-Lint, may require this file to be generated again.
|
||||
|
||||
linters:
|
||||
# Offense count: 944
|
||||
# Offense count: 951
|
||||
LineLength:
|
||||
enabled: false
|
||||
|
||||
@@ -15,7 +15,7 @@ linters:
|
||||
UnnecessaryStringOutput:
|
||||
enabled: false
|
||||
|
||||
# Offense count: 44
|
||||
# Offense count: 57
|
||||
RuboCop:
|
||||
enabled: false
|
||||
|
||||
@@ -26,16 +26,22 @@ linters:
|
||||
- 'app/views/admin/reports/show.html.haml'
|
||||
- 'app/views/disputes/strikes/show.html.haml'
|
||||
|
||||
# Offense count: 15
|
||||
# Offense count: 32
|
||||
InstanceVariables:
|
||||
exclude:
|
||||
- 'app/views/admin/reports/_actions.html.haml'
|
||||
- 'app/views/admin/roles/_form.html.haml'
|
||||
- 'app/views/admin/webhooks/_form.html.haml'
|
||||
- 'app/views/auth/registrations/_status.html.haml'
|
||||
- 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml'
|
||||
- 'app/views/authorize_interactions/_post_follow_actions.html.haml'
|
||||
- 'app/views/invites/_form.html.haml'
|
||||
- 'app/views/relationships/_account.html.haml'
|
||||
- 'app/views/shared/_og.html.haml'
|
||||
|
||||
# Offense count: 2
|
||||
# Offense count: 3
|
||||
IdNames:
|
||||
exclude:
|
||||
- 'app/views/authorize_interactions/error.html.haml'
|
||||
- 'app/views/oauth/authorizations/error.html.haml'
|
||||
- 'app/views/shared/_error_messages.html.haml'
|
||||
@@ -31,6 +31,9 @@
|
||||
# Ignore Vagrant files
|
||||
.vagrant/
|
||||
|
||||
# Ignore Capistrano customizations
|
||||
/config/deploy/*
|
||||
|
||||
# Ignore IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
+6
-1
@@ -28,7 +28,6 @@ AllCops:
|
||||
- 'Vagrantfile'
|
||||
- 'vendor/**/*'
|
||||
- 'lib/json_ld/*' # Generated files
|
||||
- 'lib/mastodon/migration_helpers.rb' # Vendored from GitLab
|
||||
- 'lib/templates/**/*'
|
||||
|
||||
# Reason: Prefer Hashes without extreme indentation
|
||||
@@ -76,6 +75,12 @@ Metrics/AbcSize:
|
||||
- 'lib/mastodon/cli/*.rb'
|
||||
- db/*migrate/**/*
|
||||
|
||||
# Reason:
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
|
||||
Metrics/BlockNesting:
|
||||
Exclude:
|
||||
- 'lib/mastodon/cli/*.rb'
|
||||
|
||||
# Reason: Currently disabled in .rubocop_todo.yml
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
|
||||
Metrics/CyclomaticComplexity:
|
||||
|
||||
@@ -13,6 +13,32 @@ Bundler/OrderedGems:
|
||||
Exclude:
|
||||
- 'Gemfile'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: with_first_argument, with_fixed_indentation
|
||||
Layout/ArgumentAlignment:
|
||||
Exclude:
|
||||
- 'config/initializers/cors.rb'
|
||||
- 'config/initializers/session_store.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
||||
# SupportedHashRocketStyles: key, separator, table
|
||||
# SupportedColonStyles: key, separator, table
|
||||
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
||||
Layout/HashAlignment:
|
||||
Exclude:
|
||||
- 'config/environments/production.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'config/routes.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment.
|
||||
Layout/LeadingCommentSpace:
|
||||
Exclude:
|
||||
- 'config/application.rb'
|
||||
- 'config/initializers/3_omniauth.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
|
||||
# URISchemes: http, https
|
||||
@@ -20,6 +46,14 @@ Layout/LineLength:
|
||||
Exclude:
|
||||
- 'app/models/account.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: require_no_space, require_space
|
||||
Layout/SpaceInLambdaLiteral:
|
||||
Exclude:
|
||||
- 'config/environments/production.rb'
|
||||
- 'config/initializers/content_security_policy.rb'
|
||||
|
||||
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
||||
Lint/EmptyBlock:
|
||||
Exclude:
|
||||
@@ -257,6 +291,10 @@ RSpec/MultipleMemoizedHelpers:
|
||||
RSpec/NestedGroups:
|
||||
Max: 6
|
||||
|
||||
RSpec/PendingWithoutReason:
|
||||
Exclude:
|
||||
- 'spec/models/account_spec.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
@@ -806,5 +844,6 @@ Style/TrailingCommaInHashLiteral:
|
||||
Style/WordArray:
|
||||
Exclude:
|
||||
- 'app/helpers/languages_helper.rb'
|
||||
- 'config/initializers/cors.rb'
|
||||
- 'spec/controllers/settings/imports_controller_spec.rb'
|
||||
- 'spec/models/form/import_spec.rb'
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'capistrano/setup'
|
||||
require 'capistrano/deploy'
|
||||
require 'capistrano/scm/git'
|
||||
|
||||
install_plugin Capistrano::SCM::Git
|
||||
|
||||
require 'capistrano/rbenv'
|
||||
require 'capistrano/bundler'
|
||||
require 'capistrano/yarn'
|
||||
require 'capistrano/rails/assets'
|
||||
require 'capistrano/rails/migrations'
|
||||
|
||||
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
|
||||
ARG NODE_VERSION="20.8-bookworm-slim"
|
||||
ARG NODE_VERSION="20.6-bookworm-slim"
|
||||
|
||||
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby
|
||||
FROM node:${NODE_VERSION} as build
|
||||
|
||||
@@ -170,6 +170,12 @@ group :development do
|
||||
# Linter CLI for HAML files
|
||||
gem 'haml_lint', require: false
|
||||
|
||||
# Deployment automation
|
||||
gem 'capistrano', '~> 3.17'
|
||||
gem 'capistrano-rails', '~> 1.6'
|
||||
gem 'capistrano-rbenv', '~> 2.2'
|
||||
gem 'capistrano-yarn', '~> 2.0'
|
||||
|
||||
# Validate missing i18n keys
|
||||
gem 'i18n-tasks', '~> 1.0', require: false
|
||||
end
|
||||
|
||||
+39
-12
@@ -84,9 +84,9 @@ GEM
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_model_serializers (0.10.14)
|
||||
actionpack (>= 4.1)
|
||||
activemodel (>= 4.1)
|
||||
active_model_serializers (0.10.13)
|
||||
actionpack (>= 4.1, < 7.1)
|
||||
activemodel (>= 4.1, < 7.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (7.0.8)
|
||||
@@ -112,6 +112,8 @@ GEM
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
airbrussh (1.4.1)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
android_key_attestation (0.3.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
@@ -173,6 +175,21 @@ GEM
|
||||
bundler-audit (0.9.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 1.0)
|
||||
capistrano (3.17.3)
|
||||
airbrussh (>= 1.0.0)
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (2.1.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-rails (1.6.3)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (>= 1.1, < 3)
|
||||
capistrano-rbenv (2.2.0)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (3.39.2)
|
||||
addressable
|
||||
matrix
|
||||
@@ -307,7 +324,7 @@ GEM
|
||||
ruby-progressbar (~> 1.4)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
haml (6.2.0)
|
||||
haml (6.1.2)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
@@ -316,8 +333,8 @@ GEM
|
||||
activesupport (>= 5.1)
|
||||
haml (>= 4.0.6)
|
||||
railties (>= 5.1)
|
||||
haml_lint (0.51.0)
|
||||
haml (>= 4.0)
|
||||
haml_lint (0.50.0)
|
||||
haml (>= 4.0, < 6.2)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
rubocop (>= 1.0)
|
||||
@@ -456,8 +473,11 @@ GEM
|
||||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
timeout
|
||||
net-scp (4.0.0)
|
||||
net-ssh (>= 2.6.5, < 8.0.0)
|
||||
net-smtp (0.3.3)
|
||||
net-protocol
|
||||
net-ssh (7.1.0)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.4)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
@@ -620,7 +640,7 @@ GEM
|
||||
sidekiq (>= 5, < 8)
|
||||
rspec-support (3.12.1)
|
||||
rspec_chunked (0.6)
|
||||
rubocop (1.56.4)
|
||||
rubocop (1.56.3)
|
||||
base64 (~> 0.1.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
@@ -666,12 +686,12 @@ GEM
|
||||
scenic (1.7.0)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
selenium-webdriver (4.13.1)
|
||||
selenium-webdriver (4.11.0)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
semantic_range (3.0.0)
|
||||
sidekiq (6.5.11)
|
||||
sidekiq (6.5.9)
|
||||
connection_pool (>= 2.2.5, < 3)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.5.0, < 5)
|
||||
@@ -706,6 +726,9 @@ GEM
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.21.5)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
stackprof (0.2.25)
|
||||
statsd-ruby (1.5.0)
|
||||
stoplight (3.0.2)
|
||||
@@ -724,7 +747,7 @@ GEM
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
test-prof (1.2.3)
|
||||
thor (1.2.2)
|
||||
tilt (2.3.0)
|
||||
tilt (2.2.0)
|
||||
timeout (0.4.0)
|
||||
tpm-key_attestation (0.12.0)
|
||||
bindata (~> 2.4)
|
||||
@@ -750,7 +773,7 @@ GEM
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.5.0)
|
||||
unicode-display_width (2.4.2)
|
||||
uri (0.12.2)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
@@ -781,7 +804,7 @@ GEM
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
websocket (1.2.10)
|
||||
websocket (1.2.9)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
@@ -806,6 +829,10 @@ DEPENDENCIES
|
||||
brakeman (~> 6.0)
|
||||
browser
|
||||
bundler-audit (~> 0.9)
|
||||
capistrano (~> 3.17)
|
||||
capistrano-rails (~> 1.6)
|
||||
capistrano-rbenv (~> 2.2)
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.39)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
chewy (~> 7.3)
|
||||
|
||||
@@ -15,7 +15,6 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ---------------- |
|
||||
| 4.2.x | Yes |
|
||||
| 4.1.x | Yes |
|
||||
| 4.0.x | Until 2023-10-31 |
|
||||
| 3.5.x | Until 2023-12-31 |
|
||||
|
||||
@@ -5,7 +5,15 @@ class AboutController < ApplicationController
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :set_instance_presenter
|
||||
|
||||
def show
|
||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
end
|
||||
@@ -11,6 +11,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
before_action :set_pack
|
||||
before_action :set_sessions, only: [:edit, :update]
|
||||
before_action :set_strikes, only: [:edit, :update]
|
||||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||
before_action :require_not_suspended!, only: [:update]
|
||||
before_action :set_cache_headers, only: [:edit, :update]
|
||||
@@ -111,6 +112,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
|
||||
include TwoFactorAuthenticationConcern
|
||||
|
||||
before_action :set_instance_presenter, only: [:new]
|
||||
before_action :set_body_classes
|
||||
|
||||
content_security_policy only: :new do |p|
|
||||
@@ -103,6 +104,10 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
use_pack 'auth'
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'lighter'
|
||||
end
|
||||
|
||||
@@ -9,11 +9,17 @@ module AccountControllerConcern
|
||||
FOLLOW_PER_PAGE = 12
|
||||
|
||||
included do
|
||||
before_action :set_instance_presenter
|
||||
|
||||
after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def set_link_headers
|
||||
response.headers['Link'] = LinkHeader.new(
|
||||
[
|
||||
|
||||
@@ -4,11 +4,11 @@ module WebAppControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
vary_by 'Accept, Accept-Language, Cookie'
|
||||
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
prepend_before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_pack
|
||||
before_action :set_app_body_class
|
||||
|
||||
vary_by 'Accept, Accept-Language, Cookie'
|
||||
end
|
||||
|
||||
def skip_csrf_meta_tags?
|
||||
@@ -23,10 +23,8 @@ module WebAppControllerConcern
|
||||
return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in
|
||||
|
||||
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
||||
return if redirect_path.blank?
|
||||
|
||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||
redirect_to(redirect_path)
|
||||
redirect_to(redirect_path) if redirect_path.present?
|
||||
end
|
||||
|
||||
def set_pack
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class FollowerAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class FollowingAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
class HomeController < ApplicationController
|
||||
include WebAppControllerConcern
|
||||
|
||||
before_action :set_instance_presenter
|
||||
|
||||
def index
|
||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,15 @@ class PrivacyController < ApplicationController
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :set_instance_presenter
|
||||
|
||||
def show
|
||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
end
|
||||
@@ -10,6 +10,7 @@ class StatusesController < ApplicationController
|
||||
|
||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_status
|
||||
before_action :set_instance_presenter
|
||||
before_action :redirect_to_original, only: :show
|
||||
before_action :set_body_classes, only: :embed
|
||||
|
||||
@@ -67,6 +68,10 @@ class StatusesController < ApplicationController
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def redirect_to_original
|
||||
redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog?
|
||||
end
|
||||
|
||||
@@ -14,6 +14,7 @@ class TagsController < ApplicationController
|
||||
before_action :set_local
|
||||
before_action :set_tag
|
||||
before_action :set_statuses, if: -> { request.format == :rss }
|
||||
before_action :set_instance_presenter
|
||||
|
||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||
|
||||
@@ -48,6 +49,10 @@ class TagsController < ApplicationController
|
||||
@statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status)
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def limit_param
|
||||
params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||
end
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin::AnnouncementsHelper
|
||||
def datetime_pattern
|
||||
'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}'
|
||||
end
|
||||
|
||||
def datetime_placeholder
|
||||
Time.zone.now.strftime('%FT%R')
|
||||
end
|
||||
end
|
||||
@@ -1,24 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DatabaseHelper
|
||||
def replica_enabled?
|
||||
ENV['REPLICA_DB_NAME'] || ENV.fetch('REPLICA_DATABASE_URL', nil)
|
||||
end
|
||||
module_function :replica_enabled?
|
||||
|
||||
def with_read_replica(&block)
|
||||
if replica_enabled?
|
||||
ApplicationRecord.connected_to(role: :reading, prevent_writes: true, &block)
|
||||
else
|
||||
yield
|
||||
end
|
||||
ApplicationRecord.connected_to(role: :reading, prevent_writes: true, &block)
|
||||
end
|
||||
|
||||
def with_primary(&block)
|
||||
if replica_enabled?
|
||||
ApplicationRecord.connected_to(role: :writing, &block)
|
||||
else
|
||||
yield
|
||||
end
|
||||
ApplicationRecord.connected_to(role: :writing, &block)
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module InvitesHelper
|
||||
def invites_max_uses_options
|
||||
[1, 5, 10, 25, 50, 100]
|
||||
end
|
||||
|
||||
def invites_expires_options
|
||||
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week]
|
||||
end
|
||||
end
|
||||
@@ -230,24 +230,6 @@ module LanguagesHelper
|
||||
'sr-Latn': 'Srpski (latinica)',
|
||||
}.freeze
|
||||
|
||||
# Helper for self.sorted_locale_keys
|
||||
private_class_method def self.locale_name_for_sorting(locale)
|
||||
if locale.blank? || locale == 'und'
|
||||
'000'
|
||||
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
|
||||
ASCIIFolding.new.fold(supported_locale[1]).downcase
|
||||
elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
|
||||
ASCIIFolding.new.fold(regional_locale).downcase
|
||||
else
|
||||
locale
|
||||
end
|
||||
end
|
||||
|
||||
# Sort locales by native name for dropdown menus
|
||||
def self.sorted_locale_keys(locale_keys)
|
||||
locale_keys.sort_by { |key, _| locale_name_for_sorting(key) }
|
||||
end
|
||||
|
||||
def native_locale_name(locale)
|
||||
if locale.blank? || locale == 'und'
|
||||
I18n.t('generic.none')
|
||||
@@ -272,7 +254,6 @@ module LanguagesHelper
|
||||
|
||||
def valid_locale_or_nil(str)
|
||||
return if str.blank?
|
||||
return str if valid_locale?(str)
|
||||
|
||||
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ module MascotHelper
|
||||
full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instance_presenter
|
||||
@instance_presenter ||= InstancePresenter.new
|
||||
end
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
module RoutingHelper
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include Rails.application.routes.url_helpers
|
||||
include ActionView::Helpers::AssetTagHelper
|
||||
include Webpacker::Helper
|
||||
|
||||
included do
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def default_url_options
|
||||
ActionMailer::Base.default_url_options
|
||||
end
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
module SettingsHelper
|
||||
def filterable_languages
|
||||
LanguagesHelper.sorted_locale_keys(LanguagesHelper::SUPPORTED_LOCALES.keys)
|
||||
end
|
||||
|
||||
def ui_languages
|
||||
LanguagesHelper.sorted_locale_keys(I18n.available_locales)
|
||||
LanguagesHelper::SUPPORTED_LOCALES.keys
|
||||
end
|
||||
|
||||
def session_device_icon(session)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
export function openDropdownMenu(id, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
|
||||
}
|
||||
|
||||
export function closeDropdownMenu(id) {
|
||||
return { type: DROPDOWN_MENU_CLOSE, id };
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const openDropdownMenu = createAction<{
|
||||
id: string;
|
||||
keyboard: boolean;
|
||||
scrollKey: string;
|
||||
}>('dropdownMenu/open');
|
||||
|
||||
export const closeDropdownMenu = createAction<{ id: string }>(
|
||||
'dropdownMenu/close',
|
||||
);
|
||||
@@ -96,7 +96,7 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
||||
normalStatus.media_attachments.forEach(item => {
|
||||
const oldItem = list.find(i => i.get('id') === item.id);
|
||||
if (oldItem && oldItem.get('description') === item.description) {
|
||||
item.translation = oldItem.get('translation');
|
||||
item.translation = oldItem.get('translation')
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -129,13 +129,13 @@ export function normalizePoll(poll, normalOldPoll) {
|
||||
...option,
|
||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||
};
|
||||
}
|
||||
|
||||
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
|
||||
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
|
||||
}
|
||||
|
||||
return normalOption;
|
||||
return normalOption
|
||||
});
|
||||
|
||||
return normalPoll;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { ModalProps } from 'flavours/glitch/reducers/modal';
|
||||
|
||||
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
|
||||
|
||||
export type ModalType = keyof typeof MODAL_COMPONENTS;
|
||||
|
||||
interface OpenModalPayload {
|
||||
modalType: ModalType;
|
||||
modalProps: ModalProps;
|
||||
modalProps: unknown;
|
||||
}
|
||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||
|
||||
|
||||
@@ -9,12 +9,11 @@ import api from 'flavours/glitch/api';
|
||||
import { roundTo10 } from 'flavours/glitch/utils/numbers';
|
||||
|
||||
const dateForCohort = cohort => {
|
||||
const timeZone = 'UTC';
|
||||
switch(cohort.frequency) {
|
||||
case 'day':
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
||||
default:
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class Column extends PureComponent {
|
||||
if (scrollable.classList.contains('scrollable--flex')) {
|
||||
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!scrollable) {
|
||||
return;
|
||||
|
||||
+4
-9
@@ -4,14 +4,9 @@ import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dro
|
||||
import { fetchHistory } from 'flavours/glitch/actions/history';
|
||||
import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('flavours/glitch/store').RootState} state
|
||||
* @param {*} props
|
||||
*/
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
openDropdownId: state.dropdownMenu.openId,
|
||||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
items: state.getIn(['history', statusId, 'items']),
|
||||
loading: state.getIn(['history', statusId, 'loading']),
|
||||
});
|
||||
@@ -20,11 +15,11 @@ const mapDispatchToProps = (dispatch, { statusId }) => ({
|
||||
|
||||
onOpen (id, onItemClick, keyboard) {
|
||||
dispatch(fetchHistory(statusId));
|
||||
dispatch(openDropdownMenu({ id, keyboard }));
|
||||
dispatch(openDropdownMenu(id, keyboard));
|
||||
},
|
||||
|
||||
onClose (id) {
|
||||
dispatch(closeDropdownMenu({ id }));
|
||||
dispatch(closeDropdownMenu(id));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ class Poll extends ImmutablePureComponent {
|
||||
|
||||
handleReveal = () => {
|
||||
this.setState({ revealed: true });
|
||||
};
|
||||
}
|
||||
|
||||
renderOption (option, optionIndex, showResults) {
|
||||
const { poll, lang, disabled, intl } = this.props;
|
||||
|
||||
@@ -23,14 +23,9 @@ const MOUSE_IDLE_DELAY = 300;
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('flavours/glitch/store').RootState} state
|
||||
* @param {*} props
|
||||
*/
|
||||
const mapStateToProps = (state, { scrollKey }) => {
|
||||
return {
|
||||
preventScroll: scrollKey === state.dropdownMenu.scrollKey,
|
||||
preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -78,7 +73,7 @@ class ScrollableList extends PureComponent {
|
||||
const clientHeight = this.getClientHeight();
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,9 @@ import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
||||
|
||||
import { isUserTouching } from '../is_mobile';
|
||||
|
||||
/**
|
||||
* @param {import('flavours/glitch/store').RootState} state
|
||||
*/
|
||||
const mapStateToProps = state => ({
|
||||
openDropdownId: state.dropdownMenu.openId,
|
||||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
@@ -23,7 +20,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
actions: items,
|
||||
onClick: onItemClick,
|
||||
},
|
||||
}) : openDropdownMenu({ id, keyboard, scrollKey }));
|
||||
}) : openDropdownMenu(id, keyboard, scrollKey));
|
||||
},
|
||||
|
||||
onClose(id) {
|
||||
@@ -31,7 +28,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
modalType: 'ACTIONS',
|
||||
ignoreFocus: false,
|
||||
}));
|
||||
dispatch(closeDropdownMenu({ id }));
|
||||
dispatch(closeDropdownMenu(id));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -59,14 +59,14 @@ class Search extends PureComponent {
|
||||
};
|
||||
|
||||
defaultOptions = [
|
||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
|
||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
|
||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
|
||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
|
||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
|
||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
|
||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
|
||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
|
||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
|
||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
|
||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
|
||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
|
||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
|
||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
|
||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
|
||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } }
|
||||
];
|
||||
|
||||
setRef = c => {
|
||||
@@ -92,7 +92,7 @@ class Search extends PureComponent {
|
||||
|
||||
if (value.length > 0 || submitted) {
|
||||
onClear();
|
||||
this.setState({ options: [], selectedOption: -1 });
|
||||
this.setState({ options: [], selectedOption: -1 })
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
||||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
minWidth: `${18 * 1.28571429}px`,
|
||||
width: `${18 * 1.28571429}px`,
|
||||
};
|
||||
|
||||
export default class TextIconButton extends PureComponent {
|
||||
|
||||
@@ -80,7 +80,7 @@ class Results extends PureComponent {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
handleSelectAll = () => {
|
||||
const { submittedType, dispatch } = this.props;
|
||||
@@ -116,7 +116,7 @@ class Results extends PureComponent {
|
||||
}
|
||||
|
||||
this.setState({ type: 'hashtags' });
|
||||
};
|
||||
}
|
||||
|
||||
handleSelectStatuses = () => {
|
||||
const { submittedType, dispatch } = this.props;
|
||||
@@ -128,7 +128,7 @@ class Results extends PureComponent {
|
||||
}
|
||||
|
||||
this.setState({ type: 'statuses' });
|
||||
};
|
||||
}
|
||||
|
||||
handleLoadMoreAccounts = () => this._loadMore('accounts');
|
||||
handleLoadMoreStatuses = () => this._loadMore('statuses');
|
||||
|
||||
@@ -221,7 +221,7 @@ const Firehose = ({ feedType, multiColumn }) => {
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Firehose.propTypes = {
|
||||
multiColumn: PropTypes.bool,
|
||||
|
||||
@@ -39,17 +39,8 @@ const getHomeFeedSpeed = createSelector([
|
||||
], (statusIds, pendingStatusIds, statusMap) => {
|
||||
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||
|
||||
if (statuses.isEmpty()) {
|
||||
return {
|
||||
gap: 0,
|
||||
newest: new Date(0),
|
||||
};
|
||||
}
|
||||
|
||||
const datetimes = statuses.map(status => status.get('created_at', 0));
|
||||
const oldest = new Date(datetimes.min());
|
||||
const newest = new Date(datetimes.max());
|
||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||
|
||||
return {
|
||||
@@ -64,10 +55,8 @@ const homeTooSlow = createSelector([
|
||||
getHomeFeedSpeed,
|
||||
], (isLoading, isPartial, speed) =>
|
||||
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||
&& (
|
||||
(speed.gap > (30 * 60) // If the average gap between posts is more than 30 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||
)
|
||||
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
||||
@@ -27,9 +27,9 @@ const mapStateToProps = (state, { accountId }) => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onSignupClick() {
|
||||
dispatch(closeModal({
|
||||
modalType: undefined,
|
||||
ignoreFocus: false,
|
||||
}));
|
||||
modalType: undefined,
|
||||
ignoreFocus: false,
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
||||
},
|
||||
});
|
||||
@@ -187,7 +187,7 @@ class LoginForm extends React.PureComponent {
|
||||
|
||||
setIFrameRef = (iframe) => {
|
||||
this.iframeRef = iframe;
|
||||
};
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ expanded: true });
|
||||
|
||||
@@ -104,7 +104,7 @@ const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedD
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Comment.propTypes = {
|
||||
comment: PropTypes.string.isRequired,
|
||||
|
||||
@@ -217,7 +217,6 @@ class Status extends ImmutablePureComponent {
|
||||
componentDidMount () {
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||
this._scrollStatusIntoView();
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
@@ -615,10 +614,10 @@ class Status extends ImmutablePureComponent {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
_scrollStatusIntoView () {
|
||||
const { status, multiColumn } = this.props;
|
||||
componentDidUpdate (prevProps) {
|
||||
const { status, ancestorsIds, multiColumn } = this.props;
|
||||
|
||||
if (status) {
|
||||
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
|
||||
|
||||
@@ -635,14 +634,6 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { status, ancestorsIds } = this.props;
|
||||
|
||||
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
|
||||
this._scrollStatusIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
detachFullscreenListener(this.onFullScreenChange);
|
||||
}
|
||||
@@ -651,22 +642,6 @@ class Status extends ImmutablePureComponent {
|
||||
this.setState({ fullscreen: isFullscreen() });
|
||||
};
|
||||
|
||||
shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
// Do not change scroll when opening a modal
|
||||
if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scroll to focused post if it is loaded
|
||||
const child = this.node?.querySelector('.detailed-status__wrapper');
|
||||
if (child) {
|
||||
return [0, child.offsetTop];
|
||||
}
|
||||
|
||||
// Do not scroll otherwise, `componentDidUpdate` will take care of that
|
||||
return false;
|
||||
};
|
||||
|
||||
render () {
|
||||
let ancestors, descendants;
|
||||
const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
||||
@@ -726,7 +701,7 @@ class Status extends ImmutablePureComponent {
|
||||
)}
|
||||
/>
|
||||
|
||||
<ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||
<ScrollContainer scrollKey='thread'>
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
|
||||
{ancestors}
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ class Header extends PureComponent {
|
||||
|
||||
if (sso_redirect) {
|
||||
content = (
|
||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||
);
|
||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||
)
|
||||
} else {
|
||||
let signupButton;
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class LinkFooter extends PureComponent {
|
||||
{DividingCircle}
|
||||
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
||||
{DividingCircle}
|
||||
<span class='version'>v{version}</span>
|
||||
v{version}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -128,7 +128,7 @@ export default class ModalRoot extends PureComponent {
|
||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||
{(SpecificComponent) => {
|
||||
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
|
||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;
|
||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />
|
||||
}}
|
||||
</BundleContainer>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const SignInBanner = () => {
|
||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
|
||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (registrationsOpen) {
|
||||
|
||||
@@ -80,7 +80,7 @@ const mapStateToProps = state => ({
|
||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||
isWide: state.getIn(['local_settings', 'stretch']),
|
||||
dropdownMenuIsOpen: state.dropdownMenu.openId !== null,
|
||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||
unreadNotifications: state.getIn(['notifications', 'unread']),
|
||||
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
|
||||
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Immutable from 'immutable';
|
||||
|
||||
import {
|
||||
DROPDOWN_MENU_OPEN,
|
||||
DROPDOWN_MENU_CLOSE,
|
||||
} from '../actions/dropdown_menu';
|
||||
|
||||
const initialState = Immutable.Map({ openId: null, keyboard: false, scroll_key: null });
|
||||
|
||||
export default function dropdownMenu(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case DROPDOWN_MENU_OPEN:
|
||||
return state.merge({ openId: action.id, keyboard: action.keyboard, scroll_key: action.scroll_key });
|
||||
case DROPDOWN_MENU_CLOSE:
|
||||
return state.get('openId') === action.id ? state.set('openId', null).set('scroll_key', null) : state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
import { closeDropdownMenu, openDropdownMenu } from '../actions/dropdown_menu';
|
||||
|
||||
interface DropdownMenuState {
|
||||
openId: string | null;
|
||||
keyboard: boolean;
|
||||
scrollKey: string | null;
|
||||
}
|
||||
|
||||
const initialState: DropdownMenuState = {
|
||||
openId: null,
|
||||
keyboard: false,
|
||||
scrollKey: null,
|
||||
};
|
||||
|
||||
export const dropdownMenuReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(
|
||||
openDropdownMenu,
|
||||
(state, { payload: { id, keyboard, scrollKey } }) => {
|
||||
state.openId = id;
|
||||
state.keyboard = keyboard;
|
||||
state.scrollKey = scrollKey;
|
||||
},
|
||||
)
|
||||
.addCase(closeDropdownMenu, (state, { payload: { id } }) => {
|
||||
if (state.openId === id) {
|
||||
state.openId = null;
|
||||
state.scrollKey = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import contexts from './contexts';
|
||||
import conversations from './conversations';
|
||||
import custom_emojis from './custom_emojis';
|
||||
import domain_lists from './domain_lists';
|
||||
import { dropdownMenuReducer } from './dropdown_menu';
|
||||
import dropdown_menu from './dropdown_menu';
|
||||
import filters from './filters';
|
||||
import followed_tags from './followed_tags';
|
||||
import height_cache from './height_cache';
|
||||
@@ -49,7 +49,7 @@ import user_lists from './user_lists';
|
||||
|
||||
const reducers = {
|
||||
announcements,
|
||||
dropdownMenu: dropdownMenuReducer,
|
||||
dropdown_menu,
|
||||
timelines,
|
||||
meta,
|
||||
alerts,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Record as ImmutableRecord, Stack } from 'immutable';
|
||||
|
||||
import type { Reducer } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
|
||||
import type { ModalType } from '../actions/modal';
|
||||
import { openModal, closeModal } from '../actions/modal';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
|
||||
export type ModalProps = Record<string, unknown>;
|
||||
type ModalProps = Record<string, unknown>;
|
||||
interface Modal {
|
||||
modalType: ModalType;
|
||||
modalProps: ModalProps;
|
||||
@@ -62,22 +62,33 @@ const pushModal = (
|
||||
});
|
||||
};
|
||||
|
||||
export const modalReducer: Reducer<State> = (state = initialState, action) => {
|
||||
if (openModal.match(action))
|
||||
return pushModal(
|
||||
state,
|
||||
action.payload.modalType,
|
||||
action.payload.modalProps,
|
||||
);
|
||||
else if (closeModal.match(action)) return popModal(state, action.payload);
|
||||
// TODO: type those actions
|
||||
else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS)
|
||||
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
|
||||
else if (action.type === TIMELINE_DELETE)
|
||||
return state.update('stack', (stack) =>
|
||||
stack.filterNot(
|
||||
(modal) => modal.get('modalProps').statusId === action.id,
|
||||
),
|
||||
);
|
||||
else return state;
|
||||
};
|
||||
export function modalReducer(
|
||||
state: State = initialState,
|
||||
action: PayloadAction<{
|
||||
modalType: ModalType;
|
||||
ignoreFocus: boolean;
|
||||
modalProps: Record<string, unknown>;
|
||||
}>,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case openModal.type:
|
||||
return pushModal(
|
||||
state,
|
||||
action.payload.modalType,
|
||||
action.payload.modalProps,
|
||||
);
|
||||
case closeModal.type:
|
||||
return popModal(state, action.payload);
|
||||
case COMPOSE_UPLOAD_CHANGE_SUCCESS:
|
||||
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
|
||||
case TIMELINE_DELETE:
|
||||
return state.update('stack', (stack) =>
|
||||
stack.filterNot(
|
||||
// @ts-expect-error TIMELINE_DELETE action is not typed yet.
|
||||
(modal) => modal.get('modalProps').statusId === action.id,
|
||||
),
|
||||
);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,6 @@
|
||||
color: $dark-text-color;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.version {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -281,7 +281,6 @@
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
white-space: nowrap;
|
||||
outline: 0;
|
||||
transition: all 100ms ease-in;
|
||||
transition-property: background-color, color;
|
||||
|
||||
@@ -56,4 +56,4 @@ export const showAlertForError = (error, skipNotFound = false) => {
|
||||
title: messages.unexpectedTitle,
|
||||
message: messages.unexpectedMessage,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
export function openDropdownMenu(id, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
|
||||
}
|
||||
|
||||
export function closeDropdownMenu(id) {
|
||||
return { type: DROPDOWN_MENU_CLOSE, id };
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const openDropdownMenu = createAction<{
|
||||
id: string;
|
||||
keyboard: boolean;
|
||||
scrollKey: string;
|
||||
}>('dropdownMenu/open');
|
||||
|
||||
export const closeDropdownMenu = createAction<{ id: string }>(
|
||||
'dropdownMenu/close',
|
||||
);
|
||||
@@ -104,7 +104,7 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||
normalStatus.media_attachments.forEach(item => {
|
||||
const oldItem = list.find(i => i.get('id') === item.id);
|
||||
if (oldItem && oldItem.get('description') === item.description) {
|
||||
item.translation = oldItem.get('translation');
|
||||
item.translation = oldItem.get('translation')
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -137,13 +137,13 @@ export function normalizePoll(poll, normalOldPoll) {
|
||||
...option,
|
||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||
};
|
||||
}
|
||||
|
||||
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
|
||||
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
|
||||
}
|
||||
|
||||
return normalOption;
|
||||
return normalOption
|
||||
});
|
||||
|
||||
return normalPoll;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { ModalProps } from 'mastodon/reducers/modal';
|
||||
|
||||
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
|
||||
|
||||
export type ModalType = keyof typeof MODAL_COMPONENTS;
|
||||
|
||||
interface OpenModalPayload {
|
||||
modalType: ModalType;
|
||||
modalProps: ModalProps;
|
||||
modalProps: unknown;
|
||||
}
|
||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||
|
||||
|
||||
@@ -147,10 +147,6 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
||||
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||
|
||||
if (!signedIn) {
|
||||
if (onFailure) {
|
||||
onFailure();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||
|
||||
export interface ApiAccountFieldJSON {
|
||||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
}
|
||||
|
||||
export interface ApiAccountRoleJSON {
|
||||
color: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiAccountJSON {
|
||||
acct: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
bot: boolean;
|
||||
created_at: string;
|
||||
discoverable: boolean;
|
||||
display_name: string;
|
||||
emojis: ApiCustomEmojiJSON[];
|
||||
fields: ApiAccountFieldJSON[];
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
group: boolean;
|
||||
header: string;
|
||||
header_static: string;
|
||||
id: string;
|
||||
last_status_at: string;
|
||||
locked: boolean;
|
||||
noindex: boolean;
|
||||
note: string;
|
||||
roles: ApiAccountJSON[];
|
||||
statuses_count: number;
|
||||
uri: string;
|
||||
url: string;
|
||||
username: string;
|
||||
moved?: ApiAccountJSON;
|
||||
suspended?: boolean;
|
||||
limited?: boolean;
|
||||
memorial?: boolean;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiCustomEmojiJSON {
|
||||
shortcode: string;
|
||||
static_url: string;
|
||||
url: string;
|
||||
category?: string;
|
||||
visible_in_picker: boolean;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// See app/serializers/rest/relationship_serializer.rb
|
||||
export interface ApiRelationshipJSON {
|
||||
blocked_by: boolean;
|
||||
blocking: boolean;
|
||||
domain_blocking: boolean;
|
||||
endorsed: boolean;
|
||||
followed_by: boolean;
|
||||
following: boolean;
|
||||
id: string;
|
||||
languages: string[] | null;
|
||||
muting_notifications: boolean;
|
||||
muting: boolean;
|
||||
note: string;
|
||||
notifying: boolean;
|
||||
requested_by: boolean;
|
||||
requested: boolean;
|
||||
showing_reblogs: boolean;
|
||||
}
|
||||
@@ -45,21 +45,6 @@ describe('computeHashtagBarForStatus', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not truncate the contents when the last child is a text node', () => {
|
||||
const status = createStatus(
|
||||
'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
|
||||
['test'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('extract tags from the last line', () => {
|
||||
const status = createStatus(
|
||||
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
|
||||
|
||||
@@ -9,12 +9,11 @@ import api from 'mastodon/api';
|
||||
import { roundTo10 } from 'mastodon/utils/numbers';
|
||||
|
||||
const dateForCohort = cohort => {
|
||||
const timeZone = 'UTC';
|
||||
switch(cohort.frequency) {
|
||||
case 'day':
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
||||
default:
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,13 @@ export default class Column extends PureComponent {
|
||||
scrollable = document.scrollingElement;
|
||||
} else {
|
||||
scrollable = this.node.querySelector('.scrollable');
|
||||
}
|
||||
|
||||
// Some columns have nested `.scrollable` containers, with the outer one
|
||||
// being a wrapper while the actual scrollable content is deeper.
|
||||
if (scrollable.classList.contains('scrollable--flex')) {
|
||||
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
|
||||
}
|
||||
}
|
||||
|
||||
if (!scrollable) {
|
||||
return;
|
||||
|
||||
+4
-9
@@ -4,14 +4,9 @@ import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_m
|
||||
import { fetchHistory } from 'mastodon/actions/history';
|
||||
import DropdownMenu from 'mastodon/components/dropdown_menu';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('mastodon/store').RootState} state
|
||||
* @param {*} props
|
||||
*/
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
openDropdownId: state.dropdownMenu.openId,
|
||||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
items: state.getIn(['history', statusId, 'items']),
|
||||
loading: state.getIn(['history', statusId, 'loading']),
|
||||
});
|
||||
@@ -20,11 +15,11 @@ const mapDispatchToProps = (dispatch, { statusId }) => ({
|
||||
|
||||
onOpen (id, onItemClick, keyboard) {
|
||||
dispatch(fetchHistory(statusId));
|
||||
dispatch(openDropdownMenu({ id, keyboard }));
|
||||
dispatch(openDropdownMenu(id, keyboard));
|
||||
},
|
||||
|
||||
onClose (id) {
|
||||
dispatch(closeDropdownMenu({ id }));
|
||||
dispatch(closeDropdownMenu(id));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -109,7 +109,7 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
||||
|
||||
const lastChild = template.content.lastChild;
|
||||
|
||||
if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
|
||||
if (!lastChild) return defaultResult;
|
||||
|
||||
template.content.removeChild(lastChild);
|
||||
const contentWithoutLastLine = template;
|
||||
|
||||
@@ -132,7 +132,7 @@ class Poll extends ImmutablePureComponent {
|
||||
|
||||
handleReveal = () => {
|
||||
this.setState({ revealed: true });
|
||||
};
|
||||
}
|
||||
|
||||
renderOption (option, optionIndex, showResults) {
|
||||
const { poll, lang, disabled, intl } = this.props;
|
||||
|
||||
@@ -23,14 +23,9 @@ const MOUSE_IDLE_DELAY = 300;
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('mastodon/store').RootState} state
|
||||
* @param {*} props
|
||||
*/
|
||||
const mapStateToProps = (state, { scrollKey }) => {
|
||||
return {
|
||||
preventScroll: scrollKey === state.dropdownMenu.scrollKey,
|
||||
preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -78,7 +73,7 @@ class ScrollableList extends PureComponent {
|
||||
const clientHeight = this.getClientHeight();
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ class Status extends ImmutablePureComponent {
|
||||
} else if (attachments.getIn([0, 'type']) === 'audio') {
|
||||
return '16 / 9';
|
||||
} else {
|
||||
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2';
|
||||
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,9 @@ import { openModal, closeModal } from '../actions/modal';
|
||||
import DropdownMenu from '../components/dropdown_menu';
|
||||
import { isUserTouching } from '../is_mobile';
|
||||
|
||||
/**
|
||||
* @param {import('mastodon/store').RootState} state
|
||||
*/
|
||||
const mapStateToProps = state => ({
|
||||
openDropdownId: state.dropdownMenu.openId,
|
||||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
@@ -28,7 +25,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
actions: items,
|
||||
onClick: onItemClick,
|
||||
},
|
||||
}) : openDropdownMenu({ id, keyboard, scrollKey }));
|
||||
}) : openDropdownMenu(id, keyboard, scrollKey));
|
||||
},
|
||||
|
||||
onClose(id) {
|
||||
@@ -36,7 +33,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
modalType: 'ACTIONS',
|
||||
ignoreFocus: false,
|
||||
}));
|
||||
dispatch(closeDropdownMenu({ id }));
|
||||
dispatch(closeDropdownMenu(id));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const username = this.props.account.get('acct');
|
||||
const username = this.props.account.get('acct')
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Link to={`/@${username}`}>
|
||||
|
||||
@@ -57,14 +57,14 @@ class Search extends PureComponent {
|
||||
};
|
||||
|
||||
defaultOptions = [
|
||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
|
||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
|
||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
|
||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
|
||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
|
||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
|
||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
|
||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
|
||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
|
||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
|
||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
|
||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
|
||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
|
||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
|
||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
|
||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } }
|
||||
];
|
||||
|
||||
setRef = c => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
||||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
minWidth: `${18 * 1.28571429}px`,
|
||||
width: `${18 * 1.28571429}px`,
|
||||
};
|
||||
|
||||
export default class TextIconButton extends PureComponent {
|
||||
|
||||
@@ -67,45 +67,47 @@ class Explore extends PureComponent {
|
||||
<Search />
|
||||
</div>
|
||||
|
||||
{isSearching ? (
|
||||
<SearchResults />
|
||||
) : (
|
||||
<>
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to='/explore'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact to='/explore/tags'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
|
||||
</NavLink>
|
||||
|
||||
{signedIn && (
|
||||
<NavLink exact to='/explore/suggestions'>
|
||||
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
|
||||
<div className='scrollable scrollable--flex' data-nosnippet>
|
||||
{isSearching ? (
|
||||
<SearchResults />
|
||||
) : (
|
||||
<>
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to='/explore'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
<NavLink exact to='/explore/links'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
|
||||
</NavLink>
|
||||
</div>
|
||||
<NavLink exact to='/explore/tags'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
|
||||
</NavLink>
|
||||
|
||||
<Switch>
|
||||
<Route path='/explore/tags' component={Tags} />
|
||||
<Route path='/explore/links' component={Links} />
|
||||
<Route path='/explore/suggestions' component={Suggestions} />
|
||||
<Route exact path={['/explore', '/explore/posts', '/search']}>
|
||||
<Statuses multiColumn={multiColumn} />
|
||||
</Route>
|
||||
</Switch>
|
||||
{signedIn && (
|
||||
<NavLink exact to='/explore/suggestions'>
|
||||
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
||||
</Helmet>
|
||||
</>
|
||||
)}
|
||||
<NavLink exact to='/explore/links'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
<Route path='/explore/tags' component={Tags} />
|
||||
<Route path='/explore/links' component={Links} />
|
||||
<Route path='/explore/suggestions' component={Suggestions} />
|
||||
<Route exact path={['/explore', '/explore/posts', '/search']}>
|
||||
<Statuses multiColumn={multiColumn} />
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
||||
</Helmet>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class Links extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='explore__links scrollable' data-nosnippet>
|
||||
<div className='explore__links'>
|
||||
{banner}
|
||||
|
||||
{isLoading ? (<LoadingIndicator />) : links.map((link, i) => (
|
||||
|
||||
@@ -80,7 +80,7 @@ class Results extends PureComponent {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
handleSelectAll = () => {
|
||||
const { submittedType, dispatch } = this.props;
|
||||
@@ -116,7 +116,7 @@ class Results extends PureComponent {
|
||||
}
|
||||
|
||||
this.setState({ type: 'hashtags' });
|
||||
};
|
||||
}
|
||||
|
||||
handleSelectStatuses = () => {
|
||||
const { submittedType, dispatch } = this.props;
|
||||
@@ -128,7 +128,7 @@ class Results extends PureComponent {
|
||||
}
|
||||
|
||||
this.setState({ type: 'statuses' });
|
||||
};
|
||||
}
|
||||
|
||||
handleLoadMoreAccounts = () => this._loadMore('accounts');
|
||||
handleLoadMoreStatuses = () => this._loadMore('statuses');
|
||||
@@ -204,7 +204,7 @@ class Results extends PureComponent {
|
||||
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
|
||||
</div>
|
||||
|
||||
<div className='explore__search-results' data-nosnippet>
|
||||
<div className='explore__search-results'>
|
||||
<ScrollableList
|
||||
scrollKey='search-results'
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -42,7 +42,7 @@ class Suggestions extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='explore__suggestions scrollable' data-nosnippet>
|
||||
<div className='explore__suggestions'>
|
||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
||||
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||
))}
|
||||
|
||||
@@ -51,7 +51,7 @@ class Tags extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='scrollable explore__links' data-nosnippet>
|
||||
<div className='explore__links'>
|
||||
{banner}
|
||||
|
||||
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
||||
|
||||
@@ -169,37 +169,39 @@ const Firehose = ({ feedType, multiColumn }) => {
|
||||
<ColumnSettings />
|
||||
</ColumnHeader>
|
||||
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to='/public/local'>
|
||||
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
|
||||
</NavLink>
|
||||
<div className='scrollable scrollable--flex'>
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to='/public/local'>
|
||||
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact to='/public/remote'>
|
||||
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
|
||||
</NavLink>
|
||||
<NavLink exact to='/public/remote'>
|
||||
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact to='/public'>
|
||||
<FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
|
||||
</NavLink>
|
||||
<NavLink exact to='/public'>
|
||||
<FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={prependBanner}
|
||||
timelineId={`${feedType}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={handleLoadMore}
|
||||
trackScroll
|
||||
scrollKey='firehose'
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={prependBanner}
|
||||
timelineId={`${feedType}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={handleLoadMore}
|
||||
trackScroll
|
||||
scrollKey='firehose'
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Firehose.propTypes = {
|
||||
multiColumn: PropTypes.bool,
|
||||
|
||||
@@ -39,17 +39,8 @@ const getHomeFeedSpeed = createSelector([
|
||||
], (statusIds, pendingStatusIds, statusMap) => {
|
||||
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||
|
||||
if (statuses.isEmpty()) {
|
||||
return {
|
||||
gap: 0,
|
||||
newest: new Date(0),
|
||||
};
|
||||
}
|
||||
|
||||
const datetimes = statuses.map(status => status.get('created_at', 0));
|
||||
const oldest = new Date(datetimes.min());
|
||||
const newest = new Date(datetimes.max());
|
||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||
|
||||
return {
|
||||
@@ -64,10 +55,8 @@ const homeTooSlow = createSelector([
|
||||
getHomeFeedSpeed,
|
||||
], (isLoading, isPartial, speed) =>
|
||||
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||
&& (
|
||||
(speed.gap > (30 * 60) // If the average gap between posts is more than 30 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||
)
|
||||
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
||||
@@ -27,9 +27,9 @@ const mapStateToProps = (state, { accountId }) => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onSignupClick() {
|
||||
dispatch(closeModal({
|
||||
modalType: undefined,
|
||||
ignoreFocus: false,
|
||||
}));
|
||||
modalType: undefined,
|
||||
ignoreFocus: false,
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
||||
},
|
||||
});
|
||||
@@ -187,7 +187,7 @@ class LoginForm extends React.PureComponent {
|
||||
|
||||
setIFrameRef = (iframe) => {
|
||||
this.iframeRef = iframe;
|
||||
};
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ expanded: true });
|
||||
|
||||
@@ -45,7 +45,7 @@ class Reblogs extends ImmutablePureComponent {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleRefresh = () => {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
|
||||
@@ -104,7 +104,7 @@ const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedD
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Comment.propTypes = {
|
||||
comment: PropTypes.string.isRequired,
|
||||
|
||||
@@ -132,7 +132,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
} else if (attachments.getIn([0, 'type']) === 'audio') {
|
||||
return '16 / 9';
|
||||
} else {
|
||||
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2';
|
||||
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -220,8 +220,6 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
componentDidMount () {
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
|
||||
this._scrollStatusIntoView();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
@@ -581,10 +579,10 @@ class Status extends ImmutablePureComponent {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
_scrollStatusIntoView () {
|
||||
const { status, multiColumn } = this.props;
|
||||
componentDidUpdate (prevProps) {
|
||||
const { status, ancestorsIds, multiColumn } = this.props;
|
||||
|
||||
if (status) {
|
||||
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
|
||||
|
||||
@@ -601,14 +599,6 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { status, ancestorsIds } = this.props;
|
||||
|
||||
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
|
||||
this._scrollStatusIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
detachFullscreenListener(this.onFullScreenChange);
|
||||
}
|
||||
@@ -617,22 +607,6 @@ class Status extends ImmutablePureComponent {
|
||||
this.setState({ fullscreen: isFullscreen() });
|
||||
};
|
||||
|
||||
shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
// Do not change scroll when opening a modal
|
||||
if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scroll to focused post if it is loaded
|
||||
const child = this.node?.querySelector('.detailed-status__wrapper');
|
||||
if (child) {
|
||||
return [0, child.offsetTop];
|
||||
}
|
||||
|
||||
// Do not scroll otherwise, `componentDidUpdate` will take care of that
|
||||
return false;
|
||||
};
|
||||
|
||||
render () {
|
||||
let ancestors, descendants;
|
||||
const { isLoading, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
||||
@@ -686,7 +660,7 @@ class Status extends ImmutablePureComponent {
|
||||
)}
|
||||
/>
|
||||
|
||||
<ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||
<ScrollContainer scrollKey='thread'>
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
|
||||
{ancestors}
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@ class Header extends PureComponent {
|
||||
|
||||
if (sso_redirect) {
|
||||
content = (
|
||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||
);
|
||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||
)
|
||||
} else {
|
||||
let signupButton;
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class LinkFooter extends PureComponent {
|
||||
{DividingCircle}
|
||||
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
||||
{DividingCircle}
|
||||
<span class='version'>v{version}</span>
|
||||
v{version}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -117,7 +117,7 @@ export default class ModalRoot extends PureComponent {
|
||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||
{(SpecificComponent) => {
|
||||
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
|
||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;
|
||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />
|
||||
}}
|
||||
</BundleContainer>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const SignInBanner = () => {
|
||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
|
||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (registrationsOpen) {
|
||||
|
||||
@@ -79,7 +79,7 @@ const mapStateToProps = state => ({
|
||||
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||
dropdownMenuIsOpen: state.dropdownMenu.openId !== null,
|
||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||
username: state.getIn(['accounts', me, 'username']),
|
||||
});
|
||||
|
||||
Loaded 100 of 267 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user