From 75739a5a9b81a810196dbac2e830aa4ce5fb1139 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 10 Dec 2025 18:01:25 +0100 Subject: [PATCH 01/10] Change `build-releases` workflow to tag images `latest` based on latest `stable-x.y` branch (#37179) Co-authored-by: emilweth <7402764+emilweth@users.noreply.github.com> --- .github/workflows/build-releases.yml | 42 ++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml index 245b25a934..d2136a2de7 100644 --- a/.github/workflows/build-releases.yml +++ b/.github/workflows/build-releases.yml @@ -9,7 +9,44 @@ permissions: packages: write jobs: + check-latest-stable: + runs-on: ubuntu-latest + outputs: + latest: ${{ steps.check.outputs.is_latest_stable }} + steps: + # Repository needs to be cloned to list branches + - name: Clone repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check latest stable + shell: bash + id: check + run: | + ref="${GITHUB_REF#refs/tags/}" + + if [[ "$ref" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then + current="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + else + echo "tag $ref is not semver" + echo "is_latest_stable=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + latest=$(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/stable-*.*" \ + | sed -E 's#^origin/stable-##' \ + | sort -Vr \ + | head -n1) + + if [[ "$current" == "$latest" ]]; then + echo "is_latest_stable=true" >> "$GITHUB_OUTPUT" + else + echo "is_latest_stable=false" >> "$GITHUB_OUTPUT" + fi + build-image: + needs: check-latest-stable uses: ./.github/workflows/build-container-image.yml with: file_to_build: Dockerfile @@ -21,13 +58,14 @@ 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.5.') }} + latest=${{ needs.check-latest-stable.outputs.latest }} tags: | type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} secrets: inherit build-image-streaming: + needs: check-latest-stable uses: ./.github/workflows/build-container-image.yml with: file_to_build: streaming/Dockerfile @@ -39,7 +77,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.5.') }} + latest=${{ needs.check-latest-stable.outputs.latest }} tags: | type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} From 4eb0a506d3a2ad662a7df76543247589214e76d6 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 12 Dec 2025 13:42:43 +0100 Subject: [PATCH 02/10] Change HTTP Signature verification status from 401 to 503 on temporary failure to get remote actor (#37221) --- app/controllers/concerns/signature_verification.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 2bdd355864..1e83ab9c69 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -72,10 +72,13 @@ module SignatureVerification rescue Mastodon::SignatureVerificationError => e fail_with! e.message rescue *Mastodon::HTTP_CONNECTION_ERRORS => e + @signature_verification_failure_code ||= 503 fail_with! "Failed to fetch remote data: #{e.message}" rescue Mastodon::UnexpectedResponseError + @signature_verification_failure_code ||= 503 fail_with! 'Failed to fetch remote data (got unexpected reply from server)' rescue Stoplight::Error::RedLight + @signature_verification_failure_code ||= 503 fail_with! 'Fetching attempt skipped because of recent connection failure' end From 8233295e3b208af1df6aca086b28d8b63ffefcdd Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 16 Dec 2025 09:49:48 +0100 Subject: [PATCH 03/10] Fix mentions of domain-blocked users being processed (#37257) --- app/services/process_mentions_service.rb | 2 +- spec/services/process_mentions_service_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 6906f77e1e..c2c33689ea 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -71,7 +71,7 @@ class ProcessMentionsService < BaseService # Make sure we never mention blocked accounts unless @current_mentions.empty? mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq - blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains)) + blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains).pluck(:domain)) mentioned_account_ids = @current_mentions.map(&:account_id) blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id)) diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 3cc83d82f3..61faf3d04a 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -8,9 +8,9 @@ RSpec.describe ProcessMentionsService do let(:account) { Fabricate(:account, username: 'alice') } context 'when mentions contain blocked accounts' do - let(:non_blocked_account) { Fabricate(:account) } - let(:individually_blocked_account) { Fabricate(:account) } - let(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com') } + let!(:non_blocked_account) { Fabricate(:account) } + let!(:individually_blocked_account) { Fabricate(:account) } + let!(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com', protocol: :activitypub) } let(:status) { Fabricate(:status, account: account, text: "Hello @#{non_blocked_account.acct} @#{individually_blocked_account.acct} @#{domain_blocked_account.acct}", visibility: :public) } before do From 8d1ea4c531211947760f88f3da32831f7574a79d Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 17 Dec 2025 15:28:53 +0100 Subject: [PATCH 04/10] Fix hashtag autocomplete replacing suggestion's first characters with input (#37281) --- app/javascript/mastodon/actions/compose.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index bd1cb3ca9b..6e39db4756 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -673,7 +673,16 @@ export function selectComposeSuggestion(position, token, suggestion, path) { dispatch(useEmoji(suggestion)); } else if (suggestion.type === 'hashtag') { - completion = token + suggestion.name.slice(token.length - 1); + // TODO: it could make sense to keep the “most capitalized” of the two + const tokenName = token.slice(1); // strip leading '#' + const suggestionPrefix = suggestion.name.slice(0, tokenName.length); + const prefixMatchesSuggestion = suggestionPrefix.localeCompare(tokenName, undefined, { sensitivity: 'accent' }) === 0; + if (prefixMatchesSuggestion) { + completion = token + suggestion.name.slice(tokenName.length); + } else { + completion = `${token.slice(0, 1)}${suggestion.name}`; + } + startPosition = position - 1; } else if (suggestion.type === 'account') { completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`; From df1ab0ab9021a0508876271bbb84d30374ed61af Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 18 Dec 2025 15:26:31 +0100 Subject: [PATCH 05/10] Fix default `Admin` role not including `view_feeds` permission (#37301) --- config/roles.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/roles.yml b/config/roles.yml index 33d2635f4d..9729cdcdc7 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -14,6 +14,7 @@ admin: permissions: - view_dashboard - view_audit_log + - view_feeds - manage_users - manage_user_access - delete_user_data From 546a95349e6e81d927f49371b29575683a1635d5 Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 16 Dec 2025 17:06:59 +0100 Subject: [PATCH 06/10] Emojis: Show in embedded statuses (#37272) --- .../notifications_v2/components/embedded_status_content.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx index 9e7f66d112..845a6902a2 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx @@ -4,6 +4,7 @@ import type { List } from 'immutable'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import type { CustomEmoji } from '@/mastodon/models/custom_emoji'; import type { Status } from '@/mastodon/models/status'; import type { Mention } from './embedded_status'; @@ -33,6 +34,7 @@ export const EmbeddedStatusContent: React.FC<{ className={className} lang={status.get('language') as string} htmlString={status.get('contentHtml') as string} + extraEmojis={status.get('emoji') as List} /> ); }; From 7d9d3de972a3009993ef2092b23701651093b619 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 17 Dec 2025 15:38:46 +0100 Subject: [PATCH 07/10] Fix notifications page error in Tor browser (#37285) --- .../components/status_action_bar/remove_quote_hint.module.css | 3 +++ .../components/status_action_bar/remove_quote_hint.tsx | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 app/javascript/mastodon/components/status_action_bar/remove_quote_hint.module.css diff --git a/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.module.css b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.module.css new file mode 100644 index 0000000000..5045b6d1b9 --- /dev/null +++ b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.module.css @@ -0,0 +1,3 @@ +.inlineIcon { + vertical-align: middle; +} diff --git a/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx index dec9c3ef38..bee562603b 100644 --- a/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx +++ b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx @@ -12,6 +12,8 @@ import { Button } from '../button'; import { useDismissableBannerState } from '../dismissable_banner'; import { Icon } from '../icon'; +import classes from './remove_quote_hint.module.css'; + const DISMISSABLE_BANNER_ID = 'notifications/remove_quote_hint'; /** @@ -93,7 +95,7 @@ export const RemoveQuoteHint: React.FC<{ id: 'status.more', defaultMessage: 'More', })} - style={{ verticalAlign: 'middle' }} + className={classes.inlineIcon} /> ), }} From 962ae88caff190e866a195865036ef87c9b73bfd Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 18 Dec 2025 17:49:03 +0100 Subject: [PATCH 08/10] Fix custom emojis not displaying in CWs and fav/boost notifications (#37306) --- app/javascript/mastodon/components/content_warning.tsx | 2 +- .../notifications_v2/components/embedded_status_content.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/components/content_warning.tsx b/app/javascript/mastodon/components/content_warning.tsx index a407ec146e..2d3222f479 100644 --- a/app/javascript/mastodon/components/content_warning.tsx +++ b/app/javascript/mastodon/components/content_warning.tsx @@ -31,7 +31,7 @@ export const ContentWarning: React.FC<{ } + extraEmojis={status.get('emojis') as List} /> ); diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx index 845a6902a2..7312a94a30 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx @@ -34,7 +34,7 @@ export const EmbeddedStatusContent: React.FC<{ className={className} lang={status.get('language') as string} htmlString={status.get('contentHtml') as string} - extraEmojis={status.get('emoji') as List} + extraEmojis={status.get('emojis') as List} /> ); }; From 32c3376d84fc523094cb477084a0da99fbf29ad5 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 18 Dec 2025 18:41:21 +0100 Subject: [PATCH 09/10] Fixes CDN domain loading (#37310) --- vite.config.mts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vite.config.mts b/vite.config.mts index 3250e8f786..30946d0c2c 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -154,6 +154,14 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { }, }, }, + experimental: { + /** + * Setting this causes Vite to not rely on the base config for import URLs, + * and instead uses import.meta.url, which is what we want for proper CDN support. + * @see https://github.com/mastodon/mastodon/pull/37310 + */ + renderBuiltUrl: () => undefined, + }, worker: { format: 'es', }, From 3de59a93441367fbca0fd22818c8411adc73a967 Mon Sep 17 00:00:00 2001 From: ChaosExAnima Date: Fri, 19 Dec 2025 10:44:57 +0100 Subject: [PATCH 10/10] Remove rendering of custom emoji using the database (#37284) --- .../mastodon/features/emoji/render.test.ts | 32 +++--------- .../mastodon/features/emoji/render.ts | 49 +++++++++---------- 2 files changed, 30 insertions(+), 51 deletions(-) diff --git a/app/javascript/mastodon/features/emoji/render.test.ts b/app/javascript/mastodon/features/emoji/render.test.ts index 3c96cbfb55..af7ff75a02 100644 --- a/app/javascript/mastodon/features/emoji/render.test.ts +++ b/app/javascript/mastodon/features/emoji/render.test.ts @@ -7,6 +7,7 @@ import { stringToEmojiState, tokenizeText, } from './render'; +import type { EmojiStateCustom } from './types'; describe('tokenizeText', () => { test('returns an array of text to be a single token', () => { @@ -82,12 +83,8 @@ describe('stringToEmojiState', () => { }); }); - test('returns custom emoji state for valid custom emoji', () => { - expect(stringToEmojiState(':smile:')).toEqual({ - type: 'custom', - code: 'smile', - data: undefined, - }); + test('returns null for custom emoji without data', () => { + expect(stringToEmojiState(':smile:')).toBeNull(); }); test('returns custom emoji state with data when provided', () => { @@ -107,7 +104,6 @@ describe('stringToEmojiState', () => { test('returns null for invalid emoji strings', () => { expect(stringToEmojiState('notanemoji')).toBeNull(); - expect(stringToEmojiState(':invalid-emoji:')).toBeNull(); }); }); @@ -130,18 +126,13 @@ describe('loadEmojiDataToState', () => { }); }); - test('loads custom emoji data into state', async () => { - const dbCall = vi - .spyOn(db, 'loadCustomEmojiByShortcode') - .mockResolvedValueOnce(customEmojiFactory()); - const customState = { type: 'custom', code: 'smile' } as const; - const result = await loadEmojiDataToState(customState, 'en'); - expect(dbCall).toHaveBeenCalledWith('smile'); - expect(result).toEqual({ + test('returns null for custom emoji without data', async () => { + const customState = { type: 'custom', code: 'smile', - data: customEmojiFactory(), - }); + } as const satisfies EmojiStateCustom; + const result = await loadEmojiDataToState(customState, 'en'); + expect(result).toBeNull(); }); test('returns null if unicode emoji not found in database', async () => { @@ -151,13 +142,6 @@ describe('loadEmojiDataToState', () => { expect(result).toBeNull(); }); - test('returns null if custom emoji not found in database', async () => { - vi.spyOn(db, 'loadCustomEmojiByShortcode').mockResolvedValueOnce(undefined); - const customState = { type: 'custom', code: 'smile' } as const; - const result = await loadEmojiDataToState(customState, 'en'); - expect(result).toBeNull(); - }); - test('retries loading emoji data once if initial load fails', async () => { const dbCall = vi .spyOn(db, 'loadEmojiByHexcode') diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index 574d5ef59b..39c517b593 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -4,11 +4,7 @@ import { EMOJI_TYPE_UNICODE, EMOJI_TYPE_CUSTOM, } from './constants'; -import { - loadCustomEmojiByShortcode, - loadEmojiByHexcode, - LocaleNotLoadedError, -} from './database'; +import { loadEmojiByHexcode, LocaleNotLoadedError } from './database'; import { importEmojiData } from './loader'; import { emojiToUnicodeHex } from './normalize'; import type { @@ -79,7 +75,7 @@ export function tokenizeText(text: string): TokenizedText { export function stringToEmojiState( code: string, customEmoji: ExtraCustomEmojiMap = {}, -): EmojiState | null { +): EmojiStateUnicode | Required | null { if (isUnicodeEmoji(code)) { return { type: EMOJI_TYPE_UNICODE, @@ -89,11 +85,13 @@ export function stringToEmojiState( if (isCustomEmoji(code)) { const shortCode = code.slice(1, -1); - return { - type: EMOJI_TYPE_CUSTOM, - code: shortCode, - data: customEmoji[shortCode], - }; + if (customEmoji[shortCode]) { + return { + type: EMOJI_TYPE_CUSTOM, + code: shortCode, + data: customEmoji[shortCode], + }; + } } return null; @@ -114,26 +112,23 @@ export async function loadEmojiDataToState( return state; } + // Don't try to load data for custom emoji. + if (state.type === EMOJI_TYPE_CUSTOM) { + return null; + } + // First, try to load the data from IndexedDB. try { // This is duplicative, but that's because TS can't distinguish the state type easily. - if (state.type === EMOJI_TYPE_UNICODE) { - const data = await loadEmojiByHexcode(state.code, locale); - if (data) { - return { - ...state, - data, - }; - } - } else { - const data = await loadCustomEmojiByShortcode(state.code); - if (data) { - return { - ...state, - data, - }; - } + const data = await loadEmojiByHexcode(state.code, locale); + if (data) { + return { + ...state, + type: EMOJI_TYPE_UNICODE, + data, + }; } + // If not found, assume it's not an emoji and return null. log( 'Could not find emoji %s of type %s for locale %s',