diff --git a/.env.production.sample b/.env.production.sample
index 8ea569fb0..9ff63c49e 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -88,21 +88,3 @@ S3_ALIAS_HOST=files.example.com
# -----------------------
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952
-
-# Fetch All Replies Behavior
-# --------------------------
-
-# Period to wait between fetching replies (in minutes)
-FETCH_REPLIES_COOLDOWN_MINUTES=15
-
-# Period to wait after a post is first created before fetching its replies (in minutes)
-FETCH_REPLIES_INITIAL_WAIT_MINUTES=5
-
-# Max number of replies to fetch - total, recursively through a whole reply tree
-FETCH_REPLIES_MAX_GLOBAL=1000
-
-# Max number of replies to fetch - for a single post
-FETCH_REPLIES_MAX_SINGLE=500
-
-# Max number of replies Collection pages to fetch - total
-FETCH_REPLIES_MAX_PAGES=500
diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml
index 7608535f0..245b25a93 100644
--- a/.github/workflows/build-releases.yml
+++ b/.github/workflows/build-releases.yml
@@ -21,7 +21,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
- latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
+ latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
@@ -39,7 +39,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
- latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
+ latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
diff --git a/.gitignore b/.gitignore
index db63bc07f..4727d9ec2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
/public/packs
/public/packs-dev
/public/packs-test
+stats.html
.env
.env.production
node_modules/
diff --git a/.nvmrc b/.nvmrc
index 403f75d03..f666621e5 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-22.20
+24.10
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f63450542..f30b502ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,45 +2,61 @@
All notable changes to this project will be documented in this file.
-## [4.5.0] - UNRELEASED
+## [4.5.0] - 2025-11-06
### Added
-- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516 and #36528 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
+- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550, #36559, #36693, #36704, #36690, #36689, #36696, #36721, #36695 and #36736 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
This includes a revamp of the composer interface.\
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
-- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484 and #36481 by @ClearlyClaire, @Gargron, and @diondiondion)
+- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484, #36481, #36583, #36627 and #36547 by @ClearlyClaire, @diondiondion, @Gargron and @renchap)
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
+- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, #36607 and #36703 by @ClearlyClaire)\
+ This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds.\
+ The setting for local topic feeds has 2 values: `public` and `authenticated`. Every other setting has 3 values: `public`, `authenticated`, `disabled`.\
+ When `disabled`, users with the “View live and topic feeds” will still be able to view them.
- Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm)
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
+- Add a new server setting to choose the server landing page (#36588 and #36602 by @ClearlyClaire and @renchap)
+- Add support for `Update` activities on converted object types (#36322 by @ClearlyClaire)
- Add support for dynamic viewport height (#36272 by @e1berd)
- Add support for numeric-based URIs for new local accounts (#32724, #36304, #36316, and #36365 by @ClearlyClaire)
+- Add default visualizer for audio upload without poster (#36734 by @ChaosExAnima)
- Add Traditional Mongolian to posting languages (#36196 by @shimon1024)
- Add example post with manual quote approval policy to `dev:populate_sample_data` (#36099 by @ClearlyClaire)
- Add server-side support for handling posts with a quote policy allowing followers to quote (#36093 and #36127 by @ClearlyClaire)
- Add schema.org markup to SEO-enabled posts (#36075 by @Gargron)
- Add migration to fill unset default quote policy based on default post privacy (#36041 by @ClearlyClaire)
+- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
+- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
+- Add feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502, #36532, #36603, #36409, #36638 and #36750 by @ChaosExAnima, @ClearlyClaire and @braddunbar)\
+ This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
- Add support for exposing conversation context for new public conversations according to FEP-7888 (#35959 and #36064 by @ClearlyClaire and @jesseplusplus)
- Add digest re-check before removing followers in synchronization mechanism (#34273 by @ClearlyClaire)
-- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
- Add support for displaying Valkey version on admin dashboard (#35785 by @ykzts)
- Add delivery failure tracking and handling to FASP jobs (#35625, #35628, and #35723 by @oneiros)
- Add example of quote post with a preview card to development sample data (#35616 by @ClearlyClaire)
- Add second set of blocked text that applies to accounts regardless of account age for spam-blocking (#35563 by @ClearlyClaire)
-- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
-- Add experimental feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502 and #36532 by @ChaosExAnima and @braddunbar)\
- This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
### Changed
- Change confirmation dialogs for follow button actions “unfollow”, “unblock”, and “withdraw request” (#36289 by @diondiondion)
- Change “Follow” button labels (#36264 by @diondiondion)
- Change appearance settings to introduce new Advanced settings section (#36496 and #36506 by @diondiondion)
+- Change display of blocked and muted quoted users (#36619 by @ClearlyClaire)\
+ This adds `blocked_account`, `blocked_domain` and `muted_account` values to the `state` attribute of `Quote` and `ShallowQuote` REST API entities.
+- Change submitting an empty post to show an error rather than failing silently (#36650 by @diondiondion)
+- Change "Privacy and reach" settings from "Public profile" to their own top-level category (#27294 by @ChaelCodes)
+- Change number of times quote verification is retried to better deal with temporary failures (#36698 by @ClearlyClaire)
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
+- Change styling of column banners (#36531 by @ClearlyClaire)
+- Change recommended Node version to 24 (LTS) (#36539 by @renchap)
+- Change min. characters required for logged-out account search from 5 to 3 (#36487 by @Gargron)
+- Change browser target to Vite legacy plugin defaults (#36611 by @larouxn)
- Change index on `follows` table to improve performance of some queries (#36374 by @ClearlyClaire)
- Change links to accounts in settings and moderation views to link to local view unless account is suspended (#36340 by @diondiondion)
- Change redirection for denied registration from web app to sign-in page with error message (#36384 by @ClearlyClaire)
-- Change `timeline_preview` setting into four more granular settings (#36338, #36467 and #36497 by @ClearlyClaire)
+- Change support for RFC9421 HTTP signatures to be enabled unconditionally (#36610 by @oneiros)
- Change wording and design of interaction dialog to simplify it (#36124 by @diondiondion)
- Change dropdown menus to allow disabled items to be focused (#36078 by @diondiondion)
- Change modal background colours in light mode (#36069 by @diondiondion)
@@ -48,7 +64,7 @@ All notable changes to this project will be documented in this file.
- Change description of “Quiet public” (#36032 by @ClearlyClaire)
- Change “Boost with original visibility” to “Share again with your followers” (#36035 by @ClearlyClaire)
- Change handling of push subscriptions to automatically delete invalid ones on delivery (#35987 by @ThisIsMissEm)
-- Change design of quote posts in web UI (#35584 and #35834 by @ClearlyClaire and @Gargron)
+- Change design of quote posts in web UI (#35584 and #35834 by @Gargron)
- Change auditable accounts to be sorted by username in admin action logs interface (#35272 by @breadtk)
- Change order of translation restoration and service credit on post card (#33619 by @colindean)
- Change position of ‘add more’ to be inside table toolbar on reports (#35963 by @ThisIsMissEm)
@@ -59,6 +75,16 @@ All notable changes to this project will be documented in this file.
- Fix relationship not being fetched to evaluate whether to show a quote post (#36517 by @ClearlyClaire)
- Fix rendering of poll options in status history modal (#35633 by @ThisIsMissEm)
- Fix “mute” button being displayed to unauthenticated visitors in hashtag dropdown (#36353 by @mkljczk)
+- Fix initially selected language in Rules panel, hide selector when no alternative translations exist (#36672 by @diondiondion)
+- Fix URL comparison for mentions in case of empty path (#36613 and #36626 by @ClearlyClaire)
+- Fix hashtags not being picked up when full-width hash sign is used (#36103 and #36625 by @ClearlyClaire and @Gargron)
+- Fix layout of severed relationships when purged events are listed (#36593 by @mejofi)
+- Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716 by @ClearlyClaire)
+- Fix vacuum tasks being interrupted by a single batch failure (#36606 by @Gargron)
+- Fix handling of unreachable network error for search services (#36587 by @mjankowski)
+- Fix bookmarks export when a bookmarked status is soft-deleted (#36576 by @ClearlyClaire)
+- Fix text overflow alignment for long author names in News (#36562 by @diondiondion)
+- Fix discovery preamble missing word in admin settings (#36560 by @belatedly)
- Fix overflow handling of `.more-from-author` (#36310 by @edent)
- Fix unfortunate action button wrapping in admin area (#36247 by @diondiondion)
- Fix translate button width in Safari (#36164 and #36216 by @diondiondion)
@@ -81,6 +107,10 @@ All notable changes to this project will be documented in this file.
- Fix glitchy status keyboard navigation (#35455 and #35504 by @diondiondion)
- Fix post being submitted when pressing “Enter” in the CW field (#35445 by @diondiondion)
+### Removed
+
+- Remove support for PostgreSQL 13 (#36540 by @renchap)
+
## [4.4.8] - 2025-10-21
### Security
diff --git a/Dockerfile b/Dockerfile
index e457ae362..c64d52991 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,9 +14,9 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.7"
-# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
+# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"]
# renovate: datasource=node-version depName=node
-ARG NODE_MAJOR_VERSION="22"
+ARG NODE_MAJOR_VERSION="24"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="trixie"]
ARG DEBIAN_VERSION="trixie"
# Node.js image to use for base image based on combined variables (ex: 20-trixie-slim)
@@ -183,7 +183,7 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
-ARG VIPS_VERSION=8.17.2
+ARG VIPS_VERSION=8.17.3
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
diff --git a/Gemfile b/Gemfile
index a12e71692..7d219344b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -106,19 +106,19 @@ gem 'opentelemetry-api', '~> 1.7.0'
group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.31.0', require: false
- gem 'opentelemetry-instrumentation-active_job', '~> 0.9.0', require: false
- gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.23.0', require: false
- gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.23.0', require: false
- gem 'opentelemetry-instrumentation-excon', '~> 0.25.0', require: false
- gem 'opentelemetry-instrumentation-faraday', '~> 0.29.0', require: false
- gem 'opentelemetry-instrumentation-http', '~> 0.26.0', require: false
- gem 'opentelemetry-instrumentation-http_client', '~> 0.25.0', require: false
- gem 'opentelemetry-instrumentation-net_http', '~> 0.25.0', require: false
- gem 'opentelemetry-instrumentation-pg', '~> 0.31.0', require: false
- gem 'opentelemetry-instrumentation-rack', '~> 0.28.0', require: false
- gem 'opentelemetry-instrumentation-rails', '~> 0.38.0', require: false
- gem 'opentelemetry-instrumentation-redis', '~> 0.27.0', require: false
- gem 'opentelemetry-instrumentation-sidekiq', '~> 0.27.0', require: false
+ gem 'opentelemetry-instrumentation-active_job', '~> 0.10.0', require: false
+ gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.24.0', require: false
+ gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.24.0', require: false
+ gem 'opentelemetry-instrumentation-excon', '~> 0.26.0', require: false
+ gem 'opentelemetry-instrumentation-faraday', '~> 0.30.0', require: false
+ gem 'opentelemetry-instrumentation-http', '~> 0.27.0', require: false
+ gem 'opentelemetry-instrumentation-http_client', '~> 0.26.0', require: false
+ gem 'opentelemetry-instrumentation-net_http', '~> 0.26.0', require: false
+ gem 'opentelemetry-instrumentation-pg', '~> 0.32.0', require: false
+ gem 'opentelemetry-instrumentation-rack', '~> 0.29.0', require: false
+ gem 'opentelemetry-instrumentation-rails', '~> 0.39.0', require: false
+ gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false
+ gem 'opentelemetry-instrumentation-sidekiq', '~> 0.28.0', require: false
gem 'opentelemetry-sdk', '~> 1.4', require: false
end
diff --git a/Gemfile.lock b/Gemfile.lock
index f8d6f3055..417b89ffa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -90,7 +90,7 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
android_key_attestation (0.3.0)
- annotaterb (4.19.0)
+ annotaterb (4.20.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
ast (2.4.3)
@@ -116,7 +116,7 @@ GEM
base64 (0.3.0)
bcp47_spec (0.2.1)
bcrypt (3.1.20)
- benchmark (0.4.1)
+ benchmark (0.5.0)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
@@ -128,7 +128,7 @@ GEM
blurhash (0.1.8)
bootsnap (1.18.6)
msgpack (~> 1.2)
- brakeman (7.0.2)
+ brakeman (7.1.1)
racc
browser (6.2.0)
builder (3.3.0)
@@ -168,7 +168,7 @@ GEM
cose (1.3.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
- crack (1.0.0)
+ crack (1.0.1)
bigdecimal
rexml
crass (1.0.6)
@@ -190,10 +190,10 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
- devise-two-factor (6.1.0)
- activesupport (>= 7.0, < 8.1)
+ devise-two-factor (6.2.0)
+ activesupport (>= 7.0, < 8.2)
devise (~> 4.0)
- railties (>= 7.0, < 8.1)
+ railties (>= 7.0, < 8.2)
rotp (~> 6.0)
devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0)
@@ -224,7 +224,7 @@ GEM
mail (~> 2.7)
email_validator (2.2.4)
activemodel
- erb (5.0.2)
+ erb (5.1.3)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
@@ -337,7 +337,7 @@ GEM
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.8.1)
- irb (1.15.2)
+ irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
@@ -426,7 +426,8 @@ GEM
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
- mail (2.8.1)
+ mail (2.9.0)
+ logger
mini_mime (>= 0.1.1)
net-imap
net-pop
@@ -442,7 +443,7 @@ GEM
mime-types-data (3.2025.0924)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
- minitest (5.25.5)
+ minitest (5.26.0)
msgpack (1.8.0)
multi_json (1.17.0)
mutex_m (0.3.0)
@@ -498,74 +499,74 @@ GEM
tzinfo
validate_url
webfinger (~> 2.0)
- openssl (3.3.1)
+ openssl (3.3.2)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.7.0)
opentelemetry-common (0.23.0)
opentelemetry-api (~> 1.0)
- opentelemetry-exporter-otlp (0.31.0)
+ opentelemetry-exporter-otlp (0.31.1)
google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
- opentelemetry-sdk (~> 1.2)
+ opentelemetry-sdk (~> 1.10)
opentelemetry-semantic_conventions
opentelemetry-helpers-sql (0.2.0)
opentelemetry-api (~> 1.7)
- opentelemetry-helpers-sql-obfuscation (0.3.0)
+ opentelemetry-helpers-sql-obfuscation (0.4.0)
opentelemetry-common (~> 0.21)
- opentelemetry-instrumentation-action_mailer (0.5.0)
- opentelemetry-instrumentation-active_support (~> 0.7)
- opentelemetry-instrumentation-action_pack (0.14.1)
- opentelemetry-instrumentation-rack (~> 0.21)
- opentelemetry-instrumentation-action_view (0.10.0)
- opentelemetry-instrumentation-active_support (~> 0.7)
- opentelemetry-instrumentation-active_job (0.9.2)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-active_model_serializers (0.23.0)
+ opentelemetry-instrumentation-action_mailer (0.6.1)
+ opentelemetry-instrumentation-active_support (~> 0.10)
+ opentelemetry-instrumentation-action_pack (0.15.1)
+ opentelemetry-instrumentation-rack (~> 0.29)
+ opentelemetry-instrumentation-action_view (0.11.1)
+ opentelemetry-instrumentation-active_support (~> 0.10)
+ opentelemetry-instrumentation-active_job (0.10.1)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-active_model_serializers (0.24.0)
opentelemetry-instrumentation-active_support (>= 0.7.0)
- opentelemetry-instrumentation-active_record (0.10.1)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-active_storage (0.2.0)
- opentelemetry-instrumentation-active_support (~> 0.7)
- opentelemetry-instrumentation-active_support (0.9.1)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-base (0.24.0)
+ opentelemetry-instrumentation-active_record (0.11.1)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-active_storage (0.3.1)
+ opentelemetry-instrumentation-active_support (~> 0.10)
+ opentelemetry-instrumentation-active_support (0.10.1)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-base (0.25.0)
opentelemetry-api (~> 1.7)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1)
- opentelemetry-instrumentation-concurrent_ruby (0.23.1)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-excon (0.25.2)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-faraday (0.29.1)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-http (0.26.1)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-http_client (0.25.1)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-net_http (0.25.1)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-pg (0.31.1)
+ opentelemetry-instrumentation-concurrent_ruby (0.24.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-excon (0.26.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-faraday (0.30.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-http (0.27.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-http_client (0.26.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-net_http (0.26.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-pg (0.32.0)
opentelemetry-helpers-sql
opentelemetry-helpers-sql-obfuscation
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-rack (0.28.2)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-rails (0.38.0)
- opentelemetry-instrumentation-action_mailer (~> 0.4)
- opentelemetry-instrumentation-action_pack (~> 0.13)
- opentelemetry-instrumentation-action_view (~> 0.9)
- opentelemetry-instrumentation-active_job (~> 0.8)
- opentelemetry-instrumentation-active_record (~> 0.9)
- opentelemetry-instrumentation-active_storage (~> 0.1)
- opentelemetry-instrumentation-active_support (~> 0.8)
- opentelemetry-instrumentation-concurrent_ruby (~> 0.22)
- opentelemetry-instrumentation-redis (0.27.1)
- opentelemetry-instrumentation-base (~> 0.24)
- opentelemetry-instrumentation-sidekiq (0.27.1)
- opentelemetry-instrumentation-base (~> 0.24)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-rack (0.29.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-rails (0.39.1)
+ opentelemetry-instrumentation-action_mailer (~> 0.6)
+ opentelemetry-instrumentation-action_pack (~> 0.15)
+ opentelemetry-instrumentation-action_view (~> 0.11)
+ opentelemetry-instrumentation-active_job (~> 0.10)
+ opentelemetry-instrumentation-active_record (~> 0.11)
+ opentelemetry-instrumentation-active_storage (~> 0.3)
+ opentelemetry-instrumentation-active_support (~> 0.10)
+ opentelemetry-instrumentation-concurrent_ruby (~> 0.23)
+ opentelemetry-instrumentation-redis (0.28.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-sidekiq (0.28.0)
+ opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-registry (0.4.0)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.10.0)
@@ -620,7 +621,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
- rack (3.2.3)
+ rack (3.2.4)
rack-attack (6.8.0)
rack (>= 1.0, < 4)
rack-cors (3.0.0)
@@ -690,7 +691,7 @@ GEM
readline (~> 0.0)
rdf-normalize (0.7.0)
rdf (~> 3.3)
- rdoc (6.15.0)
+ rdoc (6.15.1)
erb
psych (>= 4.0.0)
tsort
@@ -705,9 +706,9 @@ GEM
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
- responders (3.1.1)
- actionpack (>= 5.2)
- railties (>= 5.2)
+ responders (3.2.0)
+ actionpack (>= 7.0)
+ railties (>= 7.0)
rexml (3.4.4)
rotp (6.3.0)
rouge (4.6.1)
@@ -744,7 +745,7 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
rspec-support (3.13.6)
- rubocop (1.81.1)
+ rubocop (1.81.6)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -790,7 +791,7 @@ GEM
ruby-vips (2.2.5)
ffi (~> 1.12)
logger
- rubyzip (3.2.0)
+ rubyzip (3.2.2)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.5.0)
@@ -804,7 +805,7 @@ GEM
securerandom (0.4.1)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
- sidekiq (8.0.8)
+ sidekiq (8.0.9)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
@@ -821,9 +822,9 @@ GEM
thor (>= 1.0, < 3.0)
simple-navigation (4.4.0)
activesupport (>= 2.3.2)
- simple_form (5.3.1)
- actionpack (>= 5.2)
- activemodel (>= 5.2)
+ simple_form (5.4.0)
+ actionpack (>= 7.0)
+ activemodel (>= 7.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -834,7 +835,7 @@ GEM
stackprof (0.2.27)
starry (0.2.0)
base64
- stoplight (5.3.8)
+ stoplight (5.4.0)
zeitwerk
stringio (3.1.7)
strong_migrations (2.5.1)
@@ -898,7 +899,7 @@ GEM
zeitwerk (~> 2.2)
warden (1.2.9)
rack (>= 2.0.9)
- webauthn (3.4.2)
+ webauthn (3.4.3)
android_key_attestation (~> 0.3.0)
bindata (~> 2.4)
cbor (~> 0.5.9)
@@ -910,7 +911,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
- webmock (3.25.1)
+ webmock (3.26.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -1009,19 +1010,19 @@ DEPENDENCIES
omniauth_openid_connect (~> 0.8.0)
opentelemetry-api (~> 1.7.0)
opentelemetry-exporter-otlp (~> 0.31.0)
- opentelemetry-instrumentation-active_job (~> 0.9.0)
- opentelemetry-instrumentation-active_model_serializers (~> 0.23.0)
- opentelemetry-instrumentation-concurrent_ruby (~> 0.23.0)
- opentelemetry-instrumentation-excon (~> 0.25.0)
- opentelemetry-instrumentation-faraday (~> 0.29.0)
- opentelemetry-instrumentation-http (~> 0.26.0)
- opentelemetry-instrumentation-http_client (~> 0.25.0)
- opentelemetry-instrumentation-net_http (~> 0.25.0)
- opentelemetry-instrumentation-pg (~> 0.31.0)
- opentelemetry-instrumentation-rack (~> 0.28.0)
- opentelemetry-instrumentation-rails (~> 0.38.0)
- opentelemetry-instrumentation-redis (~> 0.27.0)
- opentelemetry-instrumentation-sidekiq (~> 0.27.0)
+ opentelemetry-instrumentation-active_job (~> 0.10.0)
+ opentelemetry-instrumentation-active_model_serializers (~> 0.24.0)
+ opentelemetry-instrumentation-concurrent_ruby (~> 0.24.0)
+ opentelemetry-instrumentation-excon (~> 0.26.0)
+ opentelemetry-instrumentation-faraday (~> 0.30.0)
+ opentelemetry-instrumentation-http (~> 0.27.0)
+ opentelemetry-instrumentation-http_client (~> 0.26.0)
+ opentelemetry-instrumentation-net_http (~> 0.26.0)
+ opentelemetry-instrumentation-pg (~> 0.32.0)
+ opentelemetry-instrumentation-rack (~> 0.29.0)
+ opentelemetry-instrumentation-rails (~> 0.39.0)
+ opentelemetry-instrumentation-redis (~> 0.28.0)
+ opentelemetry-instrumentation-sidekiq (~> 0.28.0)
opentelemetry-sdk (~> 1.4)
ox (~> 2.14)
parslet
diff --git a/README.md b/README.md
index 575e7e51a..58d457b58 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ Click below to **learn more** in a video:
### Requirements
- **Ruby** 3.2+
-- **PostgreSQL** 13+
+- **PostgreSQL** 14+
- **Redis** 7.0+
- **Node.js** 20+
diff --git a/SECURITY.md b/SECURITY.md
index 19f431fac..385c94651 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -16,6 +16,6 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| Version | Supported |
| ------- | ---------------- |
| 4.4.x | Yes |
-| 4.3.x | Yes |
+| 4.3.x | Until 2026-05-06 |
| 4.2.x | Until 2026-01-08 |
| < 4.2 | No |
diff --git a/app/controllers/activitypub/quote_authorizations_controller.rb b/app/controllers/activitypub/quote_authorizations_controller.rb
index f2f5313e1..f4a150555 100644
--- a/app/controllers/activitypub/quote_authorizations_controller.rb
+++ b/app/controllers/activitypub/quote_authorizations_controller.rb
@@ -9,7 +9,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
before_action :set_quote_authorization
def show
- expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode?
+ expires_in 30.seconds, public: true if @quote.quoted_status.distributable? && public_fetch_mode?
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
@@ -23,7 +23,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
return not_found unless @quote.status.present? && @quote.quoted_status.present?
- authorize @quote.status, :show?
+ authorize @quote.quoted_status, :show?
rescue Mastodon::NotPermittedError
not_found
end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 6c4e7619b..0627acc30 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -157,7 +157,7 @@ class Api::V1::StatusesController < Api::BaseController
end
def set_quoted_status
- @quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
+ @quoted_status = Status.find(status_params[:quoted_status_id])&.proper if status_params[:quoted_status_id].present?
authorize(@quoted_status, :quote?) if @quoted_status.present?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
# TODO: distinguish between non-existing and non-quotable posts
diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx
index fea3eb0d7..dd1956446 100644
--- a/app/javascript/entrypoints/public.tsx
+++ b/app/javascript/entrypoints/public.tsx
@@ -70,7 +70,7 @@ function loaded() {
};
document.querySelectorAll('.emojify').forEach((content) => {
- content.innerHTML = emojify(content.innerHTML, {}, true); // Force emojify as public doesn't load the new emoji system.
+ content.innerHTML = emojify(content.innerHTML);
});
document
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index ccb69f0a3..232c4b1c1 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -5,6 +5,7 @@ import { throttle } from 'lodash';
import api from 'mastodon/api';
import { browserHistory } from 'mastodon/components/router';
+import { countableText } from 'mastodon/features/compose/util/counter';
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'mastodon/settings';
@@ -55,7 +56,6 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
-export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
@@ -88,6 +88,7 @@ const messages = defineMessages({
open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
+ blankPostError: { id: 'compose.error.blank_post', defaultMessage: 'Post can\'t be blank.' },
});
export const ensureComposeIsVisible = (getState) => {
@@ -197,7 +198,15 @@ export function submitCompose(successCallback) {
const hasQuote = !!getState().getIn(['compose', 'quoted_status_id']);
const spoiler_text = getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '';
- if (!(status?.length || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
+ const fulltext = `${spoiler_text ?? ''}${countableText(status ?? '')}`;
+ const hasText = fulltext.trim().length > 0;
+
+ if (!(hasText || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
+ dispatch(showAlert({
+ message: messages.blankPostError,
+ }));
+ dispatch(focusCompose());
+
return;
}
@@ -622,6 +631,7 @@ export function fetchComposeSuggestions(token) {
fetchComposeSuggestionsEmojis(dispatch, getState, token);
break;
case '#':
+ case '#':
fetchComposeSuggestionsTags(dispatch, getState, token);
break;
default:
@@ -663,11 +673,11 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
dispatch(useEmoji(suggestion));
} else if (suggestion.type === 'hashtag') {
- completion = `#${suggestion.name}`;
- startPosition = position - 1;
+ completion = suggestion.name.slice(token.length - 1);
+ startPosition = position + token.length;
} else if (suggestion.type === 'account') {
- completion = getState().getIn(['accounts', suggestion.id, 'acct']);
- startPosition = position;
+ completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`;
+ startPosition = position - 1;
}
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
@@ -727,7 +737,7 @@ function insertIntoTagHistory(recognizedTags, text) {
// complicated because of new normalization rules, it's no longer just
// a case sensitivity issue
const names = recognizedTags.map(tag => {
- const matches = text.match(new RegExp(`#${tag.name}`, 'i'));
+ const matches = text.match(new RegExp(`[##]${tag.name}`, 'i'));
if (matches && matches.length > 0) {
return matches[0].slice(1);
@@ -783,13 +793,6 @@ export function changeComposeSpoilerText(text) {
};
}
-export function changeComposeVisibility(value) {
- return {
- type: COMPOSE_VISIBILITY_CHANGE,
- value,
- };
-}
-
export function insertEmojiCompose(position, emoji, needsSpace) {
return {
type: COMPOSE_EMOJI_INSERT,
diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts
index 0f9bf5cfb..6b38b25c2 100644
--- a/app/javascript/mastodon/actions/compose_typed.ts
+++ b/app/javascript/mastodon/actions/compose_typed.ts
@@ -13,10 +13,11 @@ import {
} from 'mastodon/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
-import type { Status } from '../models/status';
+import type { Status, StatusVisibility } from '../models/status';
+import type { RootState } from '../store';
import { showAlert } from './alerts';
-import { focusCompose } from './compose';
+import { changeCompose, focusCompose } from './compose';
import { importFetchedStatuses } from './importer';
import { openModal } from './modal';
@@ -41,6 +42,10 @@ const messages = defineMessages({
id: 'quote_error.unauthorized',
defaultMessage: 'You are not authorized to quote this post.',
},
+ quoteErrorPrivateMention: {
+ id: 'quote_error.private_mentions',
+ defaultMessage: 'Quoting is not allowed with direct mentions.',
+ },
});
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
@@ -67,6 +72,39 @@ const simulateModifiedApiResponse = (
return data;
};
+export const changeComposeVisibility = createAppThunk(
+ 'compose/visibility_change',
+ (visibility: StatusVisibility, { dispatch, getState }) => {
+ if (visibility !== 'direct') {
+ return visibility;
+ }
+
+ const state = getState();
+ const quotedStatusId = state.compose.get('quoted_status_id') as
+ | string
+ | null;
+ if (!quotedStatusId) {
+ return visibility;
+ }
+
+ // Remove the quoted status
+ dispatch(quoteComposeCancel());
+ const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
+ if (!quotedStatus) {
+ return visibility;
+ }
+
+ // Append the quoted status URL to the compose text
+ const url = quotedStatus.get('url') as string;
+ const text = state.compose.get('text') as string;
+ if (!text.includes(url)) {
+ const newText = text.trim() ? `${text}\n\n${url}` : url;
+ dispatch(changeCompose(newText));
+ }
+ return visibility;
+ },
+);
+
export const changeUploadCompose = createDataLoadingThunk(
'compose/changeUpload',
async (
@@ -130,6 +168,8 @@ export const quoteComposeByStatus = createAppThunk(
if (composeState.get('id')) {
dispatch(showAlert({ message: messages.quoteErrorEdit }));
+ } else if (composeState.get('privacy') === 'direct') {
+ dispatch(showAlert({ message: messages.quoteErrorPrivateMention }));
} else if (composeState.get('poll')) {
dispatch(showAlert({ message: messages.quoteErrorPoll }));
} else if (
@@ -173,6 +213,17 @@ export const quoteComposeById = createAppThunk(
},
);
+const composeStateForbidsLink = (composeState: RootState['compose']) => {
+ return (
+ composeState.get('quoted_status_id') ||
+ composeState.get('is_submitting') ||
+ composeState.get('poll') ||
+ composeState.get('is_uploading') ||
+ composeState.get('id') ||
+ composeState.get('privacy') === 'direct'
+ );
+};
+
export const pasteLinkCompose = createDataLoadingThunk(
'compose/pasteLink',
async ({ url }: { url: string }) => {
@@ -183,15 +234,12 @@ export const pasteLinkCompose = createDataLoadingThunk(
limit: 2,
});
},
- (data, { dispatch, getState }) => {
+ (data, { dispatch, getState, requestId }) => {
const composeState = getState().compose;
if (
- composeState.get('quoted_status_id') ||
- composeState.get('is_submitting') ||
- composeState.get('poll') ||
- composeState.get('is_uploading') ||
- composeState.get('id')
+ composeStateForbidsLink(composeState) ||
+ composeState.get('fetching_link') !== requestId // Request has been cancelled
)
return;
@@ -207,6 +255,17 @@ export const pasteLinkCompose = createDataLoadingThunk(
dispatch(quoteComposeById(data.statuses[0].id));
}
},
+ {
+ useLoadingBar: false,
+ condition: (_, { getState }) =>
+ !getState().compose.get('fetching_link') &&
+ !composeStateForbidsLink(getState().compose),
+ },
+);
+
+// Ideally this would cancel the action and the HTTP request, but this is good enough
+export const cancelPasteLinkCompose = createAction(
+ 'compose/cancelPasteLinkCompose',
);
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 772337980..32c3d7666 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -1,8 +1,5 @@
import escapeTextContentForBrowser from 'escape-html';
-import { makeEmojiMap } from 'mastodon/models/custom_emoji';
-
-import emojify from '../../features/emoji/emoji';
import { expandSpoilers } from '../../initial_state';
const domParser = new DOMParser();
@@ -88,11 +85,10 @@ export function normalizeStatus(status, normalOldStatus) {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/
/g, '\n').replace(/<\/p>
/g, '\n\n');
- const emojiMap = makeEmojiMap(normalStatus.emojis);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
- normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
- normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
+ normalStatus.contentHtml = normalStatus.content;
+ normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
@@ -128,14 +124,12 @@ export function normalizeStatus(status, normalOldStatus) {
}
export function normalizeStatusTranslation(translation, status) {
- const emojiMap = makeEmojiMap(status.get('emojis').toJS());
-
const normalTranslation = {
detected_source_language: translation.detected_source_language,
language: translation.language,
provider: translation.provider,
- contentHtml: emojify(translation.content, emojiMap),
- spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
+ contentHtml: translation.content,
+ spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text),
spoiler_text: translation.spoiler_text,
};
@@ -149,9 +143,8 @@ export function normalizeStatusTranslation(translation, status) {
export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
- const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
- normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
+ normalAnnouncement.contentHtml = normalAnnouncement.content;
return normalAnnouncement;
}
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 478e0cae4..4299bad5c 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -32,13 +32,20 @@ import {
const randomUpTo = max =>
Math.floor(Math.random() * Math.floor(max));
+/**
+ * @typedef {import('mastodon/store').AppDispatch} Dispatch
+ * @typedef {import('mastodon/store').GetState} GetState
+ * @typedef {import('redux').UnknownAction} UnknownAction
+ * @typedef {function(Dispatch, GetState): Promise} FallbackFunction
+ */
+
/**
* @param {string} timelineId
* @param {string} channelName
* @param {Object.} params
* @param {Object} options
- * @param {function(Function, Function): Promise} [options.fallback]
- * @param {function(): void} [options.fillGaps]
+ * @param {FallbackFunction} [options.fallback]
+ * @param {function(): UnknownAction} [options.fillGaps]
* @param {function(object): boolean} [options.accept]
* @returns {function(): void}
*/
@@ -46,13 +53,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
const { messages } = getLocale();
return connectStream(channelName, params, (dispatch, getState) => {
+ // @ts-ignore
const locale = getState().getIn(['meta', 'locale']);
// @ts-expect-error
let pollingId;
/**
- * @param {function(Function, Function): Promise} fallback
+ * @param {FallbackFunction} fallback
*/
const useFallback = async fallback => {
@@ -132,7 +140,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
};
/**
- * @param {Function} dispatch
+ * @param {Dispatch} dispatch
*/
async function refreshHomeTimelineAndNotification(dispatch) {
await dispatch(expandHomeTimeline({ maxId: undefined }));
@@ -151,7 +159,11 @@ async function refreshHomeTimelineAndNotification(dispatch) {
* @returns {function(): void}
*/
export const connectUserStream = () =>
- connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
+ connectTimelineStream('home', 'user', {}, {
+ fallback: refreshHomeTimelineAndNotification,
+ // @ts-expect-error
+ fillGaps: fillHomeTimelineGaps
+ });
/**
* @param {Object} options
@@ -159,7 +171,10 @@ export const connectUserStream = () =>
* @returns {function(): void}
*/
export const connectCommunityStream = ({ onlyMedia } = {}) =>
- connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
+ connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, {
+ // @ts-expect-error
+ fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia }))
+ });
/**
* @param {Object} options
@@ -168,7 +183,10 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
* @returns {function(): void}
*/
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
- connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote }) });
+ connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, {
+ // @ts-expect-error
+ fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote })
+ });
/**
* @param {string} columnId
@@ -191,4 +209,7 @@ export const connectDirectStream = () =>
* @returns {function(): void}
*/
export const connectListStream = listId =>
- connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
+ connectTimelineStream(`list:${listId}`, 'list', { list: listId }, {
+ // @ts-expect-error
+ fillGaps: () => fillListTimelineGaps(listId)
+ });
diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx
index e87ae654f..6d4ab1ddd 100644
--- a/app/javascript/mastodon/components/account_bio.tsx
+++ b/app/javascript/mastodon/components/account_bio.tsx
@@ -1,11 +1,6 @@
-import { useCallback } from 'react';
-
import classNames from 'classnames';
-import { useLinks } from 'mastodon/hooks/useLinks';
-
import { useAppSelector } from '../store';
-import { isModernEmojiEnabled } from '../utils/environment';
import { EmojiHTML } from './emoji/html';
import { useElementHandledLink } from './status/handled_link';
@@ -21,22 +16,6 @@ export const AccountBio: React.FC = ({
accountId,
showDropdown = false,
}) => {
- const handleClick = useLinks(showDropdown);
- const handleNodeChange = useCallback(
- (node: HTMLDivElement | null) => {
- if (
- !showDropdown ||
- !node ||
- node.childNodes.length === 0 ||
- isModernEmojiEnabled()
- ) {
- return;
- }
- addDropdownToHashtags(node, accountId);
- },
- [showDropdown, accountId],
- );
-
const htmlHandlers = useElementHandledLink({
hashtagAccountId: showDropdown ? accountId : undefined,
});
@@ -62,30 +41,7 @@ export const AccountBio: React.FC = ({
htmlString={note}
extraEmojis={extraEmojis}
className={classNames(className, 'translate')}
- onClickCapture={handleClick}
- ref={handleNodeChange}
{...htmlHandlers}
/>
);
};
-
-function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
- if (!node) {
- return;
- }
- for (const childNode of node.childNodes) {
- if (!(childNode instanceof HTMLElement)) {
- continue;
- }
- if (
- childNode instanceof HTMLAnchorElement &&
- (childNode.classList.contains('hashtag') ||
- childNode.innerText.startsWith('#')) &&
- !childNode.dataset.menuHashtag
- ) {
- childNode.dataset.menuHashtag = accountId;
- } else if (childNode.childNodes.length > 0) {
- addDropdownToHashtags(childNode, accountId);
- }
- }
-}
diff --git a/app/javascript/mastodon/components/autosuggest_input.jsx b/app/javascript/mastodon/components/autosuggest_input.jsx
index f707a18e1..267c04421 100644
--- a/app/javascript/mastodon/components/autosuggest_input.jsx
+++ b/app/javascript/mastodon/components/autosuggest_input.jsx
@@ -61,7 +61,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
static defaultProps = {
autoFocus: true,
- searchTokens: ['@', ':', '#'],
+ searchTokens: ['@', '@', ':', '#', '#'],
};
state = {
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.jsx b/app/javascript/mastodon/components/autosuggest_textarea.jsx
index 68cf9e17f..137bad9b7 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.jsx
+++ b/app/javascript/mastodon/components/autosuggest_textarea.jsx
@@ -25,7 +25,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
word = str.slice(left, right + caretPosition);
}
- if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
+ if (!word || word.trim().length < 3 || ['@', '@', ':', '#', '#'].indexOf(word[0]) === -1) {
return [null, null];
}
diff --git a/app/javascript/mastodon/components/display_name/display_name.stories.tsx b/app/javascript/mastodon/components/display_name/display_name.stories.tsx
index d546fdd13..6f1819a55 100644
--- a/app/javascript/mastodon/components/display_name/display_name.stories.tsx
+++ b/app/javascript/mastodon/components/display_name/display_name.stories.tsx
@@ -74,6 +74,6 @@ export const Linked: Story = {
acct: username,
})
: undefined;
- return ;
+ return ;
},
};
diff --git a/app/javascript/mastodon/components/display_name/no-domain.tsx b/app/javascript/mastodon/components/display_name/no-domain.tsx
index ee6e84050..530e0a08e 100644
--- a/app/javascript/mastodon/components/display_name/no-domain.tsx
+++ b/app/javascript/mastodon/components/display_name/no-domain.tsx
@@ -9,9 +9,8 @@ import { Skeleton } from '../skeleton';
import type { DisplayNameProps } from './index';
export const DisplayNameWithoutDomain: FC<
- Omit &
- ComponentPropsWithoutRef<'span'>
-> = ({ account, className, children, ...props }) => {
+ Omit & ComponentPropsWithoutRef<'span'>
+> = ({ account, className, children, localDomain: _, ...props }) => {
return (
&
- ComponentPropsWithoutRef<'span'>
-> = ({ account, ...props }) => {
+ Omit & ComponentPropsWithoutRef<'span'>
+> = ({ account, localDomain: _, ...props }) => {
if (!account) {
return null;
}
diff --git a/app/javascript/mastodon/components/emoji/context.tsx b/app/javascript/mastodon/components/emoji/context.tsx
index 730ae743e..3682b9414 100644
--- a/app/javascript/mastodon/components/emoji/context.tsx
+++ b/app/javascript/mastodon/components/emoji/context.tsx
@@ -7,8 +7,6 @@ import {
useState,
} from 'react';
-import classNames from 'classnames';
-
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
import { autoPlayGif } from '@/mastodon/initial_state';
import { polymorphicForwardRef } from '@/types/polymorphic';
@@ -65,11 +63,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
const parentContext = useContext(AnimateEmojiContext);
if (parentContext !== null) {
return (
-
+
{children}
);
@@ -78,7 +72,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
return (
(
+export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
(
{
extraEmojis,
@@ -59,32 +56,4 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
);
},
);
-ModernEmojiHTML.displayName = 'ModernEmojiHTML';
-
-export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
- (props, ref) => {
- const {
- as: asElement,
- htmlString,
- extraEmojis,
- className,
- onElement,
- onAttribute,
- ...rest
- } = props;
- const Wrapper = asElement ?? 'div';
- return (
-
- );
- },
-);
-LegacyEmojiHTML.displayName = 'LegacyEmojiHTML';
-
-export const EmojiHTML = isModernEmojiEnabled()
- ? ModernEmojiHTML
- : LegacyEmojiHTML;
+EmojiHTML.displayName = 'EmojiHTML';
diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx
index 471d48841..b51af40e9 100644
--- a/app/javascript/mastodon/components/hover_card_account.tsx
+++ b/app/javascript/mastodon/components/hover_card_account.tsx
@@ -23,8 +23,6 @@ import { domain } from 'mastodon/initial_state';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
-import { useLinks } from '../hooks/useLinks';
-
export const HoverCardAccount = forwardRef<
HTMLDivElement,
{ accountId?: string }
@@ -66,8 +64,6 @@ export const HoverCardAccount = forwardRef<
!isMutual &&
!isFollower;
- const handleClick = useLinks();
-
return (
-
+
onParentElement?.(...args) ?? onLinkElement(...args),
[onLinkElement, onParentElement],
);
- return ;
+ return ;
},
);
diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx
index a9229e6ee..98954fc2d 100644
--- a/app/javascript/mastodon/components/poll.tsx
+++ b/app/javascript/mastodon/components/poll.tsx
@@ -13,9 +13,7 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { openModal } from 'mastodon/actions/modal';
import { fetchPoll, vote } from 'mastodon/actions/polls';
import { Icon } from 'mastodon/components/icon';
-import emojify from 'mastodon/features/emoji/emoji';
import { useIdentity } from 'mastodon/identity_context';
-import { makeEmojiMap } from 'mastodon/models/custom_emoji';
import type * as Model from 'mastodon/models/poll';
import type { Status } from 'mastodon/models/status';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
@@ -235,12 +233,11 @@ const PollOption: React.FC = (props) => {
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
if (!titleHtml) {
- const emojiMap = makeEmojiMap(poll.emojis);
- titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
+ titleHtml = escapeTextContentForBrowser(title);
}
return titleHtml;
- }, [option, poll, title]);
+ }, [option, title]);
// Handlers
const handleOptionChange = useCallback(() => {
diff --git a/app/javascript/mastodon/components/status/handled_link.tsx b/app/javascript/mastodon/components/status/handled_link.tsx
index 3c8973992..be816e985 100644
--- a/app/javascript/mastodon/components/status/handled_link.tsx
+++ b/app/javascript/mastodon/components/status/handled_link.tsx
@@ -26,7 +26,12 @@ export const HandledLink: FC> = ({
...props
}) => {
// Handle hashtags
- if (text.startsWith('#') || prevText?.endsWith('#')) {
+ if (
+ text.startsWith('#') ||
+ prevText?.endsWith('#') ||
+ text.startsWith('#') ||
+ prevText?.endsWith('#')
+ ) {
const hashtag = text.slice(1).trim();
return (
> = ({
{children}
);
- } else if ((text.startsWith('@') || prevText?.endsWith('@')) && mention) {
+ } else if (mention) {
// Handle mentions
return (
({
languages: state.getIn(['server', 'translationLanguages', 'items']),
});
+const compareUrls = (href1, href2) => {
+ try {
+ const url1 = new URL(href1);
+ const url2 = new URL(href2);
+
+ return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search;
+ } catch {
+ return false;
+ }
+};
+
class StatusContent extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
@@ -108,41 +117,6 @@ class StatusContent extends PureComponent {
onCollapsedToggle(collapsed);
}
-
- // Exit if modern emoji is enabled, as it handles links using the HandledLink component.
- if (isModernEmojiEnabled()) {
- return;
- }
-
- const links = node.querySelectorAll('a');
-
- let link, mention;
-
- for (var i = 0; i < links.length; ++i) {
- link = links[i];
-
- if (link.classList.contains('status-link')) {
- continue;
- }
-
- link.classList.add('status-link');
-
- mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
-
- if (mention) {
- link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
- link.setAttribute('title', `@${mention.get('acct')}`);
- link.setAttribute('href', `/@${mention.get('acct')}`);
- link.setAttribute('data-hover-card-account', mention.get('id'));
- } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
- link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
- link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
- link.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id']));
- } else {
- link.setAttribute('title', link.href);
- link.classList.add('unhandled-link');
- }
- }
}
componentDidMount () {
@@ -153,22 +127,6 @@ class StatusContent extends PureComponent {
this._updateStatusLinks();
}
- onMentionClick = (mention, e) => {
- if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.props.history.push(`/@${mention.get('acct')}`);
- }
- };
-
- onHashtagClick = (hashtag, e) => {
- hashtag = hashtag.replace(/^#/, '');
-
- if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.props.history.push(`/tags/${hashtag}`);
- }
- };
-
handleMouseDown = (e) => {
this.startXY = [e.clientX, e.clientY];
};
@@ -206,7 +164,7 @@ class StatusContent extends PureComponent {
handleElement = (element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) {
- const mention = this.props.status.get('mentions').find(item => element.href === item.get('url'));
+ const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url')));
return (
);
- } else if (element instanceof HTMLParagraphElement && element.classList.contains('quote-inline')) {
+ } else if (element.classList.contains('quote-inline') && this.props.status.get('quote')) {
return null;
}
return undefined;
diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx
index a0024bbf6..647672e05 100644
--- a/app/javascript/mastodon/components/status_quoted.tsx
+++ b/app/javascript/mastodon/components/status_quoted.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -83,6 +83,62 @@ const LimitedAccountHint: React.FC<{ accountId: string }> = ({ accountId }) => {
);
};
+const FilteredQuote: React.FC<{
+ reveal: VoidFunction;
+ quotedAccountId: string;
+ quoteState: string;
+}> = ({ reveal, quotedAccountId, quoteState }) => {
+ const account = useAppSelector((state) =>
+ quotedAccountId ? state.accounts.get(quotedAccountId) : undefined,
+ );
+
+ const quoteAuthorName = account?.acct;
+ const domain = quoteAuthorName?.split('@')[1];
+
+ let message;
+
+ switch (quoteState) {
+ case 'blocked_account':
+ message = (
+
+ );
+ break;
+ case 'blocked_domain':
+ message = (
+
+ );
+ break;
+ case 'muted_account':
+ message = (
+
+ );
+ }
+
+ return (
+ <>
+ {message}
+
+ >
+ );
+};
+
interface QuotedStatusProps {
quote: QuoteMap;
contextType?: string;
@@ -130,6 +186,11 @@ export const QuotedStatus: React.FC = ({
const isLoaded = loadingState === 'complete';
const isFetchingQuoteRef = useRef(false);
+ const [revealed, setRevealed] = useState(false);
+
+ const reveal = useCallback(() => {
+ setRevealed(true);
+ }, [setRevealed]);
useEffect(() => {
if (isLoaded) {
@@ -189,6 +250,20 @@ export const QuotedStatus: React.FC = ({
defaultMessage='Post removed by author'
/>
);
+ } else if (
+ (quoteState === 'blocked_account' ||
+ quoteState === 'blocked_domain' ||
+ quoteState === 'muted_account') &&
+ !revealed &&
+ accountId
+ ) {
+ quoteError = (
+
+ );
} else if (
!status ||
!quotedStatusId ||
diff --git a/app/javascript/mastodon/components/verified_badge.tsx b/app/javascript/mastodon/components/verified_badge.tsx
index 43edbc795..cc8d8bf40 100644
--- a/app/javascript/mastodon/components/verified_badge.tsx
+++ b/app/javascript/mastodon/components/verified_badge.tsx
@@ -1,30 +1,10 @@
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
-import { isModernEmojiEnabled } from '../utils/environment';
import type { OnAttributeHandler } from '../utils/html';
import { Icon } from './icon';
-const domParser = new DOMParser();
-
-const stripRelMe = (html: string) => {
- if (isModernEmojiEnabled()) {
- return html;
- }
- const document = domParser.parseFromString(html, 'text/html').documentElement;
-
- document.querySelectorAll('a[rel]').forEach((link) => {
- link.rel = link.rel
- .split(' ')
- .filter((x: string) => x !== 'me')
- .join(' ');
- });
-
- const body = document.querySelector('body');
- return body?.innerHTML ?? '';
-};
-
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
if (name === 'rel' && tagName === 'a') {
if (value === 'me') {
@@ -47,10 +27,6 @@ interface Props {
export const VerifiedBadge: React.FC = ({ link }) => (
-
+
);
diff --git a/app/javascript/mastodon/features/about/components/rules.tsx b/app/javascript/mastodon/features/about/components/rules.tsx
index e413adb31..078fb68c0 100644
--- a/app/javascript/mastodon/features/about/components/rules.tsx
+++ b/app/javascript/mastodon/features/about/components/rules.tsx
@@ -32,16 +32,38 @@ interface Rule extends BaseRule {
translations?: Record;
}
+function getDefaultSelectedLocale(
+ currentUiLocale: string,
+ localeOptions: SelectItem[],
+) {
+ const preciseMatch = localeOptions.find(
+ (option) => option.value === currentUiLocale,
+ );
+ if (preciseMatch) {
+ return preciseMatch.value;
+ }
+
+ const partialLocale = currentUiLocale.split('-')[0];
+ const partialMatch = localeOptions.find(
+ (option) => option.value.split('-')[0] === partialLocale,
+ );
+
+ return partialMatch?.value ?? 'default';
+}
+
export const RulesSection: FC = ({ isLoading = false }) => {
const intl = useIntl();
- const [locale, setLocale] = useState(intl.locale);
- const rules = useAppSelector((state) => rulesSelector(state, locale));
const localeOptions = useAppSelector((state) =>
localeOptionsSelector(state, intl),
);
+ const [selectedLocale, setSelectedLocale] = useState(() =>
+ getDefaultSelectedLocale(intl.locale, localeOptions),
+ );
+ const rules = useAppSelector((state) => rulesSelector(state, selectedLocale));
+
const handleLocaleChange: ChangeEventHandler = useCallback(
(e) => {
- setLocale(e.currentTarget.value);
+ setSelectedLocale(e.currentTarget.value);
},
[],
);
@@ -74,25 +96,27 @@ export const RulesSection: FC = ({ isLoading = false }) => {
))}
-
-
-
-
+ {localeOptions.length > 1 && (
+
+
+
+
+ )}
);
};
diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
index 2bf636d06..040ca16c7 100644
--- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
@@ -49,7 +49,6 @@ import { ShortNumber } from 'mastodon/components/short_number';
import { AccountNote } from 'mastodon/features/account/components/account_note';
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
-import { useLinks } from 'mastodon/hooks/useLinks';
import { useIdentity } from 'mastodon/identity_context';
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
@@ -198,7 +197,6 @@ export const AccountHeader: React.FC<{
state.relationships.get(accountId),
);
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
- const handleLinkClick = useLinks();
const handleBlock = useCallback(() => {
if (!account) {
@@ -852,10 +850,7 @@ export const AccountHeader: React.FC<{
{!(suspended || hidden) && (
-
+
{account.id !== me && signedIn && (
)}
diff --git a/app/javascript/mastodon/features/audio/index.tsx b/app/javascript/mastodon/features/audio/index.tsx
index a6a131c0d..c16fd9eab 100644
--- a/app/javascript/mastodon/features/audio/index.tsx
+++ b/app/javascript/mastodon/features/audio/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useCallback, useState, useId } from 'react';
+import { useEffect, useRef, useCallback, useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@@ -22,6 +22,8 @@ import { useAudioVisualizer } from 'mastodon/hooks/useAudioVisualizer';
import { displayMedia, useBlurhash } from 'mastodon/initial_state';
import { playerSettings } from 'mastodon/settings';
+import { AudioVisualizer } from './visualizer';
+
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
@@ -116,7 +118,6 @@ export const Audio: React.FC<{
const seekRef = useRef
(null);
const volumeRef = useRef(null);
const hoverTimeoutRef = useRef | null>();
- const accessibilityId = useId();
const { audioContextRef, sourceRef, gainNodeRef, playAudio, pauseAudio } =
useAudioContext({ audioElementRef: audioRef });
@@ -538,19 +539,6 @@ export const Audio: React.FC<{
[togglePlay, toggleMute],
);
- const springForBand0 = useSpring({
- to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
- config: config.wobbly,
- });
- const springForBand1 = useSpring({
- to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
- config: config.wobbly,
- });
- const springForBand2 = useSpring({
- to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
- config: config.wobbly,
- });
-
const progress = Math.min((currentTime / loadedDuration) * 100, 100);
const effectivelyMuted = muted || volume === 0;
@@ -641,81 +629,7 @@ export const Audio: React.FC<{
-
+
+
+ {isQuotePost && visibility === 'direct' && (
+
+
+
+
+ )}