diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml
index ced5ecfe884..217415c528b 100644
--- a/.devcontainer/compose.yaml
+++ b/.devcontainer/compose.yaml
@@ -73,7 +73,7 @@ services:
hard: -1
libretranslate:
- image: libretranslate/libretranslate:v1.6.2
+ image: libretranslate/libretranslate:v1.7.3
restart: unless-stopped
volumes:
- lt-data:/home/libretranslate/.local
diff --git a/.env.production.sample b/.env.production.sample
index 8ea569fb016..9ff63c49ef1 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/actions/setup-javascript/action.yml b/.github/actions/setup-javascript/action.yml
index 808adc7de64..0c7ead1c15f 100644
--- a/.github/actions/setup-javascript/action.yml
+++ b/.github/actions/setup-javascript/action.yml
@@ -9,7 +9,7 @@ runs:
using: 'composite'
steps:
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index c1a1c99eb70..2bffaaf27ba 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -23,8 +23,6 @@
// Require Dependency Dashboard Approval for major version bumps of these node packages
matchManagers: ['npm'],
matchPackageNames: [
- 'tesseract.js', // Requires code changes
-
// react-router: Requires manual upgrade
'history',
'react-router-dom',
diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml
index 260730004cc..84b729df434 100644
--- a/.github/workflows/build-container-image.yml
+++ b/.github/workflows/build-container-image.yml
@@ -35,7 +35,7 @@ jobs:
- linux/arm64
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Prepare
env:
@@ -100,7 +100,7 @@ jobs:
- name: Upload digest
if: ${{ inputs.push_to_images != '' }}
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
# `hashFiles` is used to disambiguate between streaming and non-streaming images
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
@@ -119,10 +119,10 @@ jobs:
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Download digests
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v6
with:
path: ${{ runner.temp }}/digests
# `hashFiles` is used to disambiguate between streaming and non-streaming images
diff --git a/.github/workflows/build-push-pr.yml b/.github/workflows/build-push-pr.yml
index 418993475f4..f6ff6521753 100644
--- a/.github/workflows/build-push-pr.yml
+++ b/.github/workflows/build-push-pr.yml
@@ -18,7 +18,7 @@ jobs:
steps:
# Repository needs to be cloned so `git rev-parse` below works
- name: Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- id: version_vars
run: |
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml
index db17b2169ca..245b25a9344 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.3.') }}
+ 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.3.') }}
+ latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml
index fa28d28f740..9cc49a7f797 100644
--- a/.github/workflows/bundler-audit.yml
+++ b/.github/workflows/bundler-audit.yml
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up Ruby
uses: ruby/setup-ruby@v1
diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml
index c46090c1b56..9d500ffc44e 100644
--- a/.github/workflows/check-i18n.yml
+++ b/.github/workflows/check-i18n.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml
index 4e6179bc774..e0383b83bcc 100644
--- a/.github/workflows/chromatic.yml
+++ b/.github/workflows/chromatic.yml
@@ -23,7 +23,7 @@ jobs:
if: github.repository == 'mastodon/mastodon'
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Javascript environment
@@ -33,7 +33,7 @@ jobs:
run: yarn build-storybook
- name: Run Chromatic
- uses: chromaui/action@v12
+ uses: chromaui/action@v13
with:
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index c864e12d2d8..cf038ae4809 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -31,11 +31,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -48,7 +48,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v3
+ uses: github/codeql-action/autobuild@v4
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -61,6 +61,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v4
with:
category: '/language:${{matrix.language}}'
diff --git a/.github/workflows/crowdin-download-stable.yml b/.github/workflows/crowdin-download-stable.yml
index 8e18a9d0a0f..8a6ac6df277 100644
--- a/.github/workflows/crowdin-download-stable.yml
+++ b/.github/workflows/crowdin-download-stable.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Increase Git http.postBuffer
# This is needed due to a bug in Ubuntu's cURL version?
diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml
index d247a514d92..89125fb2db4 100644
--- a/.github/workflows/crowdin-download.yml
+++ b/.github/workflows/crowdin-download.yml
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Increase Git http.postBuffer
# This is needed due to a bug in Ubuntu's cURL version?
diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml
index d0d79d91996..e2764b1ec75 100644
--- a/.github/workflows/crowdin-upload.yml
+++ b/.github/workflows/crowdin-upload.yml
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: crowdin action
uses: crowdin/github-action@v2
diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml
index c10f350a02e..6803686b4ff 100644
--- a/.github/workflows/format-check.yml
+++ b/.github/workflows/format-check.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml
index c1385bf789b..51a78d679fc 100644
--- a/.github/workflows/lint-css.yml
+++ b/.github/workflows/lint-css.yml
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml
index 499be2010ad..d3452c9ffc2 100644
--- a/.github/workflows/lint-haml.yml
+++ b/.github/workflows/lint-haml.yml
@@ -33,7 +33,7 @@ jobs:
steps:
- name: Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up Ruby
uses: ruby/setup-ruby@v1
diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml
index 86e9af23e7e..c9ba1a13f17 100644
--- a/.github/workflows/lint-js.yml
+++ b/.github/workflows/lint-js.yml
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml
index 87f8aee24e0..e73617d85da 100644
--- a/.github/workflows/lint-ruby.yml
+++ b/.github/workflows/lint-ruby.yml
@@ -35,7 +35,7 @@ jobs:
steps:
- name: Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up Ruby
uses: ruby/setup-ruby@v1
diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml
index 0699e6c9ef8..b8e1cc89aaa 100644
--- a/.github/workflows/test-js.yml
+++ b/.github/workflows/test-js.yml
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml
index 7aab34f0cf4..b1b94692f0c 100644
--- a/.github/workflows/test-migrations.yml
+++ b/.github/workflows/test-migrations.yml
@@ -72,7 +72,7 @@ jobs:
BUNDLE_RETRY: 3
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml
index 63d31725043..8f05812d600 100644
--- a/.github/workflows/test-ruby.yml
+++ b/.github/workflows/test-ruby.yml
@@ -32,7 +32,7 @@ jobs:
SECRET_KEY_BASE_DUMMY: 1
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
@@ -65,7 +65,7 @@ jobs:
run: |
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@v5
if: matrix.mode == 'test'
with:
path: |-
@@ -128,9 +128,9 @@ jobs:
- '3.3'
- '.ruby-version'
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v6
with:
path: './'
name: ${{ github.sha }}
@@ -230,9 +230,9 @@ jobs:
- '3.3'
- '.ruby-version'
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v6
with:
path: './'
name: ${{ github.sha }}
@@ -309,9 +309,9 @@ jobs:
- '.ruby-version'
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v6
with:
path: './'
name: ${{ github.sha }}
@@ -350,14 +350,14 @@ jobs:
- run: bin/rspec spec/system --tag streaming --tag js
- name: Archive logs
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
if: failure()
with:
name: e2e-logs-${{ matrix.ruby-version }}
path: log/
- name: Archive test screenshots
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
if: failure()
with:
name: e2e-screenshots-${{ matrix.ruby-version }}
@@ -447,9 +447,9 @@ jobs:
search-image: opensearchproject/opensearch:2
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- - uses: actions/download-artifact@v4
+ - uses: actions/download-artifact@v6
with:
path: './'
name: ${{ github.sha }}
@@ -469,14 +469,14 @@ jobs:
- run: bin/rspec --tag search
- name: Archive logs
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
if: failure()
with:
name: test-search-logs-${{ matrix.ruby-version }}
path: log/
- name: Archive test screenshots
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
if: failure()
with:
name: test-search-screenshots
diff --git a/.gitignore b/.gitignore
index db63bc07f0d..4727d9ec27f 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 f666621e500..f88da62e246 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-24.10
+24.11
diff --git a/.storybook/main.ts b/.storybook/main.ts
index bb69f0c6649..c249d1c06dc 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -31,7 +31,7 @@ const config: StorybookConfig = {
viteFinal(config) {
// For an unknown reason, Storybook does not use the root
// from the Vite config so we need to set it manually.
- config.root = resolve(__dirname, '../app/javascript');
+ config.root = resolve(import.meta.dirname, '../app/javascript');
return config;
},
};
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f634505428b..f30b502ad14 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 1c9c956b72c..c64d529918d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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 7d219344b65..e54842fcda0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -13,7 +13,7 @@ gem 'haml-rails', '~>3.0'
gem 'pg', '~> 1.5'
gem 'pghero'
-gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873
+gem 'aws-sdk-core', require: false
gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'blurhash', '~> 0.1'
gem 'fog-core', '<= 2.6.0'
@@ -114,7 +114,7 @@ group :opentelemetry do
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-pg', '~> 0.33.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
@@ -138,7 +138,7 @@ group :test do
# Browser integration testing
gem 'capybara', '~> 3.39'
gem 'capybara-playwright-driver'
- gem 'playwright-ruby-client', '1.55.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
+ gem 'playwright-ruby-client', '1.56.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
# Used to reset the database between system tests
gem 'database_cleaner-active_record'
diff --git a/Gemfile.lock b/Gemfile.lock
index e36498c3024..251cf771e7c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -90,23 +90,26 @@ 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)
attr_required (1.0.2)
aws-eventstream (1.4.0)
- aws-partitions (1.1168.0)
- aws-sdk-core (3.215.1)
+ aws-partitions (1.1180.0)
+ aws-sdk-core (3.236.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
+ base64
+ bigdecimal
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.96.0)
- aws-sdk-core (~> 3, >= 3.210.0)
+ logger
+ aws-sdk-kms (1.116.0)
+ aws-sdk-core (~> 3, >= 3.234.0)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.177.0)
- aws-sdk-core (~> 3, >= 3.210.0)
+ aws-sdk-s3 (1.203.0)
+ aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
@@ -116,7 +119,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 +131,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 +171,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 +193,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 +227,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
@@ -279,7 +282,7 @@ GEM
rake (>= 13)
googleapis-common-protos-types (1.22.0)
google-protobuf (~> 4.26)
- haml (6.3.0)
+ haml (6.4.0)
temple (>= 0.8.2)
thor
tilt
@@ -288,7 +291,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
- haml_lint (0.66.0)
+ haml_lint (0.67.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@@ -337,7 +340,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)
@@ -346,7 +349,7 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
- json (2.15.1)
+ json (2.15.2)
json-canonicalization (1.0.0)
json-jwt (1.17.0)
activesupport (>= 4.2)
@@ -443,7 +446,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)
@@ -465,7 +468,7 @@ GEM
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- oj (3.16.11)
+ oj (3.16.12)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.4)
@@ -512,9 +515,9 @@ GEM
opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.10)
opentelemetry-semantic_conventions
- opentelemetry-helpers-sql (0.2.0)
+ opentelemetry-helpers-sql (0.3.0)
opentelemetry-api (~> 1.7)
- opentelemetry-helpers-sql-obfuscation (0.4.0)
+ opentelemetry-helpers-sql-obfuscation (0.5.0)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.6.1)
opentelemetry-instrumentation-active_support (~> 0.10)
@@ -548,7 +551,7 @@ GEM
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-net_http (0.26.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-pg (0.32.0)
+ opentelemetry-instrumentation-pg (0.33.0)
opentelemetry-helpers-sql
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.25)
@@ -581,7 +584,7 @@ GEM
ox (2.14.23)
bigdecimal (>= 3.0)
parallel (1.27.0)
- parser (3.3.9.0)
+ parser (3.3.10.0)
ast (~> 2.4.1)
racc
parslet (2.0.0)
@@ -590,7 +593,7 @@ GEM
pg (1.6.2)
pghero (3.7.0)
activerecord (>= 7.1)
- playwright-ruby-client (1.55.0)
+ playwright-ruby-client (1.56.0)
concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0)
pp (0.6.3)
@@ -604,7 +607,7 @@ GEM
net-smtp
premailer (~> 1.7, >= 1.7.9)
prettyprint (0.2.0)
- prism (1.5.2)
+ prism (1.6.0)
prometheus_exporter (2.3.0)
webrick
propshaft (1.3.1)
@@ -621,7 +624,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)
@@ -691,7 +694,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
@@ -706,9 +709,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)
@@ -745,7 +748,7 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
rspec-support (3.13.6)
- rubocop (1.81.6)
+ rubocop (1.81.7)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -791,7 +794,7 @@ GEM
ruby-vips (2.2.5)
ffi (~> 1.12)
logger
- rubyzip (3.2.1)
+ rubyzip (3.2.2)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.5.0)
@@ -803,9 +806,9 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.4.1)
- shoulda-matchers (6.5.0)
- activesupport (>= 5.2.0)
- sidekiq (8.0.8)
+ shoulda-matchers (7.0.1)
+ activesupport (>= 7.1)
+ sidekiq (8.0.9)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
@@ -822,9 +825,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)
@@ -835,7 +838,7 @@ GEM
stackprof (0.2.27)
starry (0.2.0)
base64
- stoplight (5.4.0)
+ stoplight (5.5.0)
zeitwerk
stringio (3.1.7)
strong_migrations (2.5.1)
@@ -883,7 +886,7 @@ GEM
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
- uri (1.0.4)
+ uri (1.1.1)
useragent (0.16.11)
validate_url (1.0.15)
activemodel (>= 3.0.0)
@@ -911,7 +914,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
- webmock (3.25.1)
+ webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -933,7 +936,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10)
addressable (~> 2.8)
annotaterb (~> 4.13)
- aws-sdk-core (< 3.216.0)
+ aws-sdk-core
aws-sdk-s3 (~> 1.123)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
@@ -1018,7 +1021,7 @@ DEPENDENCIES
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-pg (~> 0.33.0)
opentelemetry-instrumentation-rack (~> 0.29.0)
opentelemetry-instrumentation-rails (~> 0.39.0)
opentelemetry-instrumentation-redis (~> 0.28.0)
@@ -1028,7 +1031,7 @@ DEPENDENCIES
parslet
pg (~> 1.5)
pghero
- playwright-ruby-client (= 1.55.0)
+ playwright-ruby-client (= 1.56.0)
premailer-rails
prometheus_exporter (~> 2.2)
propshaft
diff --git a/SECURITY.md b/SECURITY.md
index 19f431fac59..12052652e6c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -15,7 +15,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| Version | Supported |
| ------- | ---------------- |
+| 4.5.x | Yes |
| 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 f2f5313e1ad..f4a15055508 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 0627acc30f8..fd7757f2e81 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -66,7 +66,7 @@ class Api::V1::StatusesController < Api::BaseController
if async_refresh.running?
add_async_refresh_header(async_refresh)
elsif !current_account.nil? && @status.should_fetch_replies?
- add_async_refresh_header(AsyncRefresh.create(refresh_key))
+ add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
WorkerBatch.new.within do |batch|
batch.connect(refresh_key, threshold: 1.0)
@@ -126,10 +126,11 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.where(account: current_account).find(params[:id])
authorize @status, :destroy?
+ json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
+
@status.discard_with_reblogs
StatusPin.find_by(status: @status)&.destroy
@status.account.statuses_count = @status.account.statuses_count - 1
- json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index e9727b756a4..0e0062876a7 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -7,6 +7,7 @@ class FollowerAccountsController < ApplicationController
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :protect_hidden_collections, if: -> { request.format.json? }
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :limited_federation_mode?
@@ -18,8 +19,6 @@ class FollowerAccountsController < ApplicationController
end
format.json do
- raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
-
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: collection_presenter,
@@ -41,6 +40,10 @@ class FollowerAccountsController < ApplicationController
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
end
+ def protect_hidden_collections
+ raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
+ end
+
def page_requested?
params[:page].present?
end
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 803d6e342a9..7a0f37887de 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -7,6 +7,7 @@ class FollowingAccountsController < ApplicationController
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :protect_hidden_collections, if: -> { request.format.json? }
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :limited_federation_mode?
@@ -18,11 +19,6 @@ class FollowingAccountsController < ApplicationController
end
format.json do
- if page_requested? && @account.hide_collections?
- forbidden
- next
- end
-
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: collection_presenter,
@@ -44,6 +40,10 @@ class FollowingAccountsController < ApplicationController
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
end
+ def protect_hidden_collections
+ raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
+ end
+
def page_requested?
params[:page].present?
end
diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx
index fea3eb0d792..dd1956446da 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 d6de589e903..232c4b1c19c 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;
}
@@ -784,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 0f9bf5cfb3c..6b38b25c25c 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 7723379804c..32c3d766665 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 478e0cae453..4299bad5c32 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 e87ae654fdf..6d4ab1ddd49 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/alert/index.tsx b/app/javascript/mastodon/components/alert/index.tsx
index 72fee0a4a30..8bee99130f5 100644
--- a/app/javascript/mastodon/components/alert/index.tsx
+++ b/app/javascript/mastodon/components/alert/index.tsx
@@ -49,7 +49,11 @@ export const Alert: React.FC<{
{hasAction && (
-